Category Archives: Windows

Certificate Services operations fail with error 0x80070057

While implementing a two-tier PKI I ran into the issue that certutil.exe -crl, and PowerShell cmdlets such as Get-CACrlDistributionPoint would fail on the Subordinate Domain CA with a generic error which made finding a solution very difficult:

PS C:\Windows\system32> Get-CACrlDistributionPoint
Get-CACrlDistributionPoint : CCertAdmin::GetConfigEntry: The parameter is incorrect. 0x80070057 (WIN32:
At line:1 char:1
+ Get-CACrlDistributionPoint
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-CACrlDistributionPoint], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException,Microsoft.CertificateServices.Administration.Comm

PS C:\Windows\system32> certutil -crl
CertUtil: -CRL command FAILED: 0x80070057 (WIN32: 87 ERROR_INVALID_PARAMETER)
CertUtil: The parameter is incorrect.
PS C:\Windows\system32>

I had started off by following this guide on Technet Blogs:

Not long after proceeding I realised that I needed to alter certain aspects of the way it was configured. I started again and continued to use the same CA server hostnames but with new CA names, this time preferring to follow this guide by Derek Seaman:

However I encountered the errors with the subordinate CA refusing to run the PowerShell cmdlets relating to the Certificate Authority. The errors were also encountered by commenter “Per” on Derek’s blog post, and similarly reported in the comments on the Windows Server 2012 R2 Active Directory Certificate Services Microsoft Test Lab page:

There is a Microsoft KB referencing the same error at the time of creating the subordinate CA. The article implicates permissions, however this is a red herring.

It took a lot of trial and error, but eventually I did resolve this issue thanks to some pointers in a Microsoft Directory Services Team Blog post on troubleshooting Certificate Enrollment. I determined the root cause – several superfluous entries in Active Directory for an aborted CA installation. I needed to delete these with ADSIEdit, though I have subsequently discovered that you can also use the AD Sites and Services MMC snap-in to do this (at parent, View > Show Services Node). When I first attempted to set up the CAs I had been using the standard auto-generated names because I had thought that not doing so might invite trouble later on – so my subordinate domain CA had published itself to Active Directory at CN=Enrollment Services,CN=Public Key Services,CN=Services using the name domain-HOSTNAME-CA.

I had thought this entry was sane when I was looking back over the ADSIEdit output while investigating the problem – because I know it formats the cert request using this notation. Then I remembered that I had not used this CA name in my subsequent CA installation attempt. I removed this old name entry from Active Directory and it immediately fixed the issue. I guess because although it was for a different CA installation attempt, crucially it shared the same server hostname, hence the problem when PowerShell was invoking Certificate Services to query the Directory Service.

Following this, I then pruned similar superfluous records for the same abortive CA installation attempt which were located at:
CN=KRA,CN=Public Key Services,CN=Services
CN=AIA,CN=Public Key Services,CN=Services
CN=MY_CA_HOSTNAME,CN=CDP,CN=Public Key Services,CN=Services

Securing access to Microsoft Exchange 2013 EAC

The coexistence of the Exchange 2013 Administration Console (EAC) with the other Exchange website virtual directories represents a considerable security vulnerability for any organization that installs it using the out-of-box defaults. Since most organizations need Outlook Anywhere and EWS to be Web-facing, and usually OWA too, the EAC will also end up being publicly accessible – inadvisable security practice in itself, even more so for another important reason that I will explain. The EAC uses the IIS Virtual Directory /ecp which has other non-admin functions for normal email users, so it is not really desirable to try to limit access to it. Besides, an Exchange Service Pack or Cumulative Update is quite likely to reset the Virtual Directory settings and permissions later anyway. Now that the Exchange Management Console application has been retired, it is not practical to completely disable EAC unless you especially enjoy PowerShell, so we need to find a way to harden the server.

The problem is that the Domain’s built-in Administrator account does not have the Active Directory account lockout policy applied to it, so the EAC can simply be brute-force attacked if this account has access to the EAC or indeed OWA. One mitigation against this vulnerability is to make sure that the domain’s built-in Administrator account has all Exchange remote access disabled, and that a separate user account is used for day-to-day management. It is probably best to also disable the mailbox entirely to reduce the chance of someone accidentally re-enabling any of this in the EAC later.

Set-CASMailbox Administrator -OWAEnabled $false -ECPEnabled $false -ActiveSyncEnabled $false -OWAforDevicesEnabled $false -EwsEnabled $false -ImapEnabled $false -PopEnabled $false

Disable-Mailbox Administrator

Note that merely disabling the Administrator mailbox (without the first step of amending the access) offers no protection – the ECP can still be accessed and you won’t be able to use the Set-CASMailbox cmdlet, since no mailbox object exists.

There is still a big security problem though. Since the authentication is being handled by IIS the usernames are not being screened, and so a user encounters an HTTP 403 error when they are barred from using ECP but have been authenticated successfully (even if you remove the Administrator account from the ‘Organization Management’ Exchange Security Group), so the brute-force attacker can saturate the server with logon requests and precisely determine when they have cracked the Domain Admin credentials. Although these credentials cannot be used remotely if the above mitigation steps have been taken, the attacker can still use them later to fully penetrate the organization via other means: social engineering, physically entering the building etc.

Microsoft’s recommended solution is to use Powershell to designate a whole CAS server’s ECP Virtual Directory for Internal use only (keeping ECP disabled on the public facing CAS servers). This is totally unworkable for most small-to-medium enterprises though.

The only valid mitigation therefore is to restrict access to the ECP virtual directory to local subnets – something that we had wanted to avoid, and which on first sight looks impossible (since it’s a Virtual Directory, not a Website that we can re-bind to a new IP address and firewall more restrictively). In order to do this you will need to install the IIS Security Feature ‘IP and Domain Restrictions’:


For the Default Web Site’s /ecp Virtual Directory (which is the public one), configure IP Address and Domain Restrictions:


In there, click Edit Feature Settings… (in the right-hand pane) and set ‘Access for unspecified clients’ to Deny. Then use Add Allow Entry to define your permitted IP ranges.

As I mentioned in the opening paragraph though this will need to be checked after Service Packs and Cumulative Updates are applied to the Exchange Server, in case this configuration is lost.

In fact the same precautions against brute force attack of the Administrator account would also apply to earlier versions of Exchange, and for VPN connectivity – i.e. when AD accounts are being used for any public facing authentication, the built-in Administrator should never be granted remote access.

PowerShell for EAP-PEAP secured SSTP VPN on Windows 8.1

Simple VPN configurations can be deployed by Group Policy but EAP authentication settings cannot be configured like this, even using Windows 8.1 and Windows Server 2012 R2. Microsoft added some new PowerShell cmdlets to Windows 8.1 for configuring VPNs, but the worked examples do not appear to function for all the settings for PEAP connections, and they do not show a worked example of how you go about exporting and re-importing a connection’s XMLStream.

Defining the XML as a block within the script itself, even assigning it as data type XML does not seem to work. Not being particularly accustomed to PowerShell, the following script took a while to get right. I assigned it as a laptop startup script by GPO. If I need to modify the connection in future I can increment the version number since the script checks the local machine Registry for that, and will not install if the desired version marker is already present.

# VPN Connection EAP-PEAP VPN provisioning 
# patters 2013

# This script is loosely based on the EAP-TTLS one published by Microsoft at
# The worked examples on that page and at
# are rudimentary, and in some details for PEAP, incorrect. To set advanced options like the TrustedRootCAs and the
# the server identity checking warning, you *must* export a GUI-authored config as XML. Configuring XML attributes alone
# will not work because some of them are missing when creating a new connection, and adding them results in errors.

# Check for marker in the Registry, and quit if found
# Desired version is 1
$version = 1
$test = Get-ItemProperty "HKLM:\Software\MyCompany" "MyCompany VPN" -ErrorAction SilentlyContinue
If ($test -eq $null) {
       $test = 0
} else {
       $test = $test."MyCompany VPN"
If ($test -ge $version) {exit} 

# VPN Connection look-up to remove any previous installations
$isTestVpn = $false
$vpnConnections = Get-VpnConnection -AllUserConnection
If($vpnConnections.Name -eq "MyCompany VPN") {Remove-VpnConnection -Name "MyCompany VPN" -AllUserConnection -Confirm:$false -Force}
$vpnConnections = Get-VpnConnection
If($vpnConnections.Name -eq "MyCompany VPN") {Remove-VpnConnection -Name "MyCompany VPN" -Confirm:$false -Force}

       #The following section documents the attempts to get this working manually before I got importing/exporting of XML working

       # says to use "New-EapConfiguration -Peap" here, but is wrong      
       #$a = New-EapConfiguration

       # Generate configuration XML for PEAP authentication method with EAP-MSCHAPv2 as its inner method
       #$b = New-EapConfiguration -Peap -VerifyServerIdentity -FastReconnect $true -TunnledEapAuthMethod $a.EapConfigXmlStream

       # Edit properties within the generated configuration XML
       #$c = $b.EapConfigXmlStream
       #$c.EapHostConfig.Config.Eap.EapType.ServerValidation.ServerNames = ""

       # Specify AddTrust Root CA for Comodo - This attribute is missing unless you create the connection using the GUI
       # The following appears to generate the XML correctly, but it won't be accepted by the Add-VpnConnection cmdlet
       #$c.EapHostConfig.Config.Eap.EapType.ServerValidation.SetAttribute("TrustedRootCA","02 fa f3 e2 91 43 54 68 60 78 57 69 4d f5 e4 5b 68 85 18 68")   

       # PeapExtensions settings are nested XML objects so setting them as string datatype will fail
       # see
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.PerformServerValidation."#text" = "true"
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.AcceptServerName."#text" = "true"
       # Once again this attribute is missing unless the connection is created using the GUI. Adding it does not work
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.PeapExtensionsV2.AllowPromptingWhenServerCANotFound."#text" = "true"      

       # Create the VPN connection ‘MyCompany VPN’ with the EAP configuration XML generated above
       #Add-VpnConnection -Name "MyCompany VPN" -ServerAddress "" -TunnelType Sstp -EncryptionLevel Maximum -AuthenticationMethod Eap -EapConfigXmlStream $c -AllUserConnection

       # FORTUNATELY THERE IS AN EASIER WAY (once you figure out PowerShell XML – why couldn’t MS have shown a worked example in the docs)...

       # Create your VPN configuration entry manually then export its XML like so:
       #$exportXML = (Get-VpnConnection -Name "My_VPN_Final" -AllUserConnection).EapConfigXmlStream

       $importXML = New-Object XML
       $importXML.Load("\\\data\Software\MyCompany VPN\MyCompany VPN.xml")
       Add-VpnConnection -Name "MyCompany VPN" -ServerAddress "" -TunnelType Sstp -EncryptionLevel Maximum -AuthenticationMethod Eap -EapConfigXmlStream $importXML -AllUserConnection
       # Leave a marker in the Registry
       If (-Not (Test-Path "HKLM:\Software\MyCompany")) {New-Item -Path "HKLM:\Software\MyCompany"}
       if (Get-ItemProperty "HKLM:\Software\MyCompany" "MyCompany VPN" -ErrorAction SilentlyContinue) {
              Set-ItemProperty -Path "HKLM:\Software\MyCompany" -Name "MyCompany VPN" -Value $version
       } else {
              New-ItemProperty -Path "HKLM:\Software\MyCompany" -Name "MyCompany VPN" -Value $version

       Write-Host "Error in connection setup!"
       Write-Host $_.Exception.Message

Tunlr enable/disable script for Microsoft Surface

I recently bought a Microsoft Surface and I have been wanting to watch a few programmes on BBC iPlayer whilst out of the country for Christmas. I discovered Tunlr – a free media proxy service which allows access to Hulu and iPlayer regardless of geolocation. However, editing DNS server settings by hand is time consuming and awkward without using the trackpad, so I wanted a script to automate the task. This will also work for other proxy services such as – just replace the DNS IPs in the script. I had previously written a quick script for changing IP configuration which used netsh commands but these don’t work on Windows RT. Some other PowerShell methods I found weren’t supported either but I did find new network settings cmdlets for the purpose that were added in Windows 8/RT.

The next problem was elevation to get sufficient rights to change the network settings. It transpires that the PowerShell and VBScript environments are heavily restricted in Windows RT, which prevents auto-prompting for elevation. Fortunately Windows RT does allow Run as Administrator from the right-click menu for .cmd scripts. If you’re using touch control, you just touch and hold then release for the right-click. The script will remind you if you forget to do this. Hover your mouse over the top right corner of the script below, and use the View Source button to save the following to your desktop as Tunlr.cmd:

@echo off

::Use Tunlr to watch streaming TV services regardless of geolocation
::Tunlr DNS servers redirect requests for well-known services via Tunlr's proxy servers
::Tunlr should only be used while watching streams to reduce server load
::More details at

::Elevation cannot be automated on Windows RT since object creation is disabled for PowerShell and VBScript

ipconfig /all | find "" > nul && (
  echo Disabling Tunlr...
  PowerShell -Command Set-DnsClientServerAddress -InterfaceAlias "WiFi" -ResetServerAddresses || (
    echo Right-click and re-run this script as Administrator
) || (
  echo Enabling Tunlr...
  PowerShell -Command Set-DnsClientServerAddress -InterfaceAlias "WiFi" -ServerAddresses, || (
    echo Right-click and re-run this script as Administrator

UAC elevation for Windows batch script

I recently needed to make an interactive batch script elevate for admin privileges. I found an example script by jagaroth, and then refined it to make it even more compact. It only writes out one temporary script file, and passes the rest of the required variables on the command line. It can cope with paths containing spaces. It was something of a shell escaping nightmare as you can see from line 14!

@echo off

::Windows XP doesn't have UAC so skip
for /f "tokens=3*" %%i in ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ProductName ^| Find "ProductName"') do set WINVER=%%i %%j 
echo %WINVER% | find "XP" > nul && goto commands

::prompt for elevation
if "%1" == "UAC" goto elevation
  echo Set objShell = CreateObject^("Shell.Application"^)
  echo Set objFSO = CreateObject^("Scripting.FileSystemObject"^)
  echo strPath = objFSO.GetParentFolderName^(WScript.ScriptFullName^)
  echo If objFSO.FileExists^("%~0"^) Then
  echo   objShell.ShellExecute "cmd.exe", "/c """"%~0"" UAC ""%~dp0""""", "", "runas", 1
  echo Else
  echo   MsgBox "Script file not found"
  echo End If
) > "%TEMP%\UAC.vbs"
cscript //nologo "%TEMP%\UAC.vbs"
goto :eof
del /q "%TEMP%\UAC.vbs"

::navigate back to this script's home folder
cd "%~p2"

::put your main script here
echo 1st arg: %1
echo 2nd arg: %2

Unified Windows PE 4.0 builder for Windows ADK

This script will build Windows PE 4.0 (for x86, or AMD64 or both) including scripts and drivers of your choosing, it will create ISO images with both BIOS and UEFI support, and will also upload the resulting WIM boot images to your WDS server automatically (and freshen them if they have been re-created). This reduces the tiresome task of boot image maintenance to just a couple of clicks.

It uses only the standard Microsoft Windows ADK tools, which is the new name for WAIK. Just save the code below as Build_WinPE.cmd and right-click on it to Run as Administrator. Notice the defined variables at the start, particularly the %SOURCE% folder. It supports using either the 32bit or the 64bit ADK, and only the Windows PE and Deployment Tools ADK components are required. The script expects the following folders:

  • %SOURCE%\scripts\WinPE – any additional scripts (e.g. OS build scripts)
  • %SOURCE%\drivers\WinPE-x86\CURRENT – drivers
  • %SOURCE%\drivers\WinPE-AMD64\CURRENT
  • %SOURCE%\tools\WinPE-x86 – optional tools such as GImageX, or apps from
  • %SOURCE%\tools\WinPE-AMD64

Notice the optional components section at lines 90-95. Modify this if you need your image to contain additional items, for instance PowerShell or .NET Framework 4.

One further observation is that Macs don’t seem to be able to boot this version of Windows PE. I’m not sure whether this is a GOP display driver issue, or whether only true UEFI firmwares are required (Macs are EFI which is an earlier specification). To carry out an unattended Windows 8 install on a Mac via BootCamp you will need to build a Windows PE 3.0 ISO since Macs can’t PXE boot.

There’s some more info about UEFI booting on 32bit architectures here – apparently UEFI 2.3.1 compliance is a requirement. My VAIO’s Insyde H2O UEFI firmware certainly seems to ignore EFI loaders.

:: Build_WinPE.cmd
:: patters 2012
:: This script will build x86 and AMD64 Windows PE 4.0, automatically
:: collecting drivers from the relevant folders within the
:: unattended installation, building WIM and ISO images, and
:: will also upload the WIM images to the deployment server(s).
:: DO NOT cancel this script in progress as you can end up with
:: orphaned locks on files inside mounted WIM images which
:: usually require a reboot of the server to clear.

@echo off

     set SOURCE=\\WDSSERVER\unattended
     set PE_TEMP=C:\temp
     ::WinPE feature pack locale
     set PL=en-US
     ::commma separated list for WDS_SERVERS
::end variables


if not exist "%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\*.*" (
     echo This script requires the Windows Assessment and Deployment Kit to be installed
     echo Download it from
     goto :eof
if "%1"=="relaunch" (
     call :BUILD_WINPE %2 %3 %4
     goto :eof
if "%1"=="unmount" (
     :: use this if you have a problem with the script and there are WIMs still mounted
     dism /Unmount-Wim /MountDir:"%PE_TEMP%\WinPE-x86\mount" /discard
     dism /Unmount-Wim /MountDir:"%PE_TEMP%\WinPE-AMD64\mount" /discard
     goto :eof
set /P SELECTION=Build WinPE for which CPU architecture (AMD64, x86, both)? [AMD64]: 
if "%SELECTION%"=="amd64" set SELECTION=AMD64
if "%SELECTION%"=="X86" set SELECTION=x86
if "%SELECTION%"=="b" set SELECTION=both
if "%SELECTION%"=="AMD64" (
     start "Building Windows PE for AMD64 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch AMD64
     goto :eof
if "%SELECTION%"=="x86" (
     start "Building Windows PE for x86 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch x86
     goto :eof
if "%SELECTION%"=="both" (
     ::opening both instances of this script simultaneously seems to cause race conditions with dism.exe
     start /wait "Building Windows PE for x86 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch x86 nopause
     start "Building Windows PE for AMD64 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch AMD64
     goto :eof
goto :prompt

set PE_ARCH=%1
set OSCDImgRoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\Oscdimg
set WinPERoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Windows Preinstallation Environment
set DandIRoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools
set DISMRoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\DISM
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\Oscdimg
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\BCDBoot
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\DISM
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Windows Preinstallation Environment
echo on
rd /s /q %PE_TEMP%\WinPE-%PE_ARCH%
call copype.cmd %PE_ARCH% %PE_TEMP%\WinPE-%PE_ARCH%
::package path
set PP=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Windows Preinstallation Environment\%PE_ARCH%\WinPE_OCs
::image path
set IP=%PE_TEMP%\WinPE-%PE_ARCH%\mount
echo on
dism /Mount-Wim /WimFile:"%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot.wim" /Index:1 /MountDir:"%IP%"
dism /image:"%IP%" /Add-Package /PackagePath:"%PP%\"^
 /PackagePath:"%PP%\%PL%\" /PackagePath:"%PP%\"^
 /PackagePath:"%PP%\%PL%\" /PackagePath:"%PP%\"^
 /PackagePath:"%PP%\%PL%\" /PackagePath:"%PP%\"^
 /PackagePath:"%PP%\%PL%\" /PackagePath:"%PP%\"^
dism /image:"%IP%" /Add-Driver /driver:"%SOURCE%\drivers\WinPE-%PE_ARCH%\CURRENT" /Recurse
copy "%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PE_ARCH%\BCDBoot\bootsect.exe" "%IP%\Windows"
copy /y "%SOURCE%\scripts\WinPE\*.*" "%IP%\Windows\System32"
copy "%SOURCE%\tools\WinPE-%PE_ARCH%\*.*" "%IP%\Windows\System32"
copy /y "%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PE_ARCH%\DISM\imagex.exe" "%IP%\Windows\System32"
dism /Unmount-Wim /MountDir:"%IP%" /commit

::Mac OS BootCamp will look for autorun.inf in order to validate this disk as a Windows Installer CD
::adding this allows us to start unattended installs using WinPE
date /T > "%PE_TEMP%\WinPE-%PE_ARCH%\media\autorun.inf"

::bootable ISO includes both BIOS & EFI boot loaders
oscdimg -m -o -u2 -udfver102 -bootdata:2#p0,e,b"%PE_TEMP%\WinPE-%PE_ARCH%\fwfiles\"#pEF,e,b"%PE_TEMP%\WinPE-%PE_ARCH%\fwfiles\efisys.bin" "%PE_TEMP%\WinPE-%PE_ARCH%\media" "%PE_TEMP%\WinPE-%PE_ARCH%\WinPE-40-%PE_ARCH%.iso"
@echo off

::rename the WIM file to avoid having multiple image files on the WDS server with the same filename
ren "%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot.wim" boot_%PE_ARCH%.wim

if "%PE_ARCH%"=="x86" set WDS_ARCH=%PE_ARCH%
if "%PE_ARCH%"=="AMD64" set WDS_ARCH=X64
for %%i in (%WDS_SERVERS%) do (
     echo Adding/updating boot image on WDS server: %%i
     :: try to add the image first, if that fails then replace existing
     wdsutil /Verbose /Progress /Add-Image /ImageFile:"%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot-40-%PE_ARCH%.wim"^
      /Server:%%i /ImageType:Boot /Name:"Microsoft Windows PE 4.0 (%PE_ARCH%)" || wdsutil /Verbose /Progress /Replace-Image^
      /Image:"Microsoft Windows PE 4.0 (%PE_ARCH%)" /ImageType:Boot /Architecture:%WDS_ARCH% /ReplacementImage^
      /Name:"Microsoft Windows PE 4.0 (%PE_ARCH%)" /ImageFile:"%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot-40-%PE_ARCH%.wim"^
::rename the WIM back again so bootable USB devices can be created
ren "%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot-40-%PE_ARCH%.wim" boot.wim
echo *******************************************************************
echo WDS boot image(s) updated
echo A bootable ISO of this image has been created at:
echo   %PE_TEMP%\WinPE-%PE_ARCH%\WinPE-40-%PE_ARCH%.iso
echo To create a bootable USB key, use diskpart.exe to create a FAT32 partition
echo and mark it active, then copy the contents of this folder to its root:
echo   %PE_TEMP%\WinPE-%PE_ARCH%\media
echo FAT32 is required for EFI support.
if "%2"=="nopause" goto :eof
goto :eof

Windows software deployment and update script

For many years I have used scripts of my own design to build workstations and to roll out software updates. At the time I created these I found that most of the tools which could accomplish these tasks were unwieldy. Group Policy software deployment in particular never really seemed fit for purpose since it extended login times so dramatically. My experience gained in a previous job spent packaging applications for deployment had taught me that all installed software populates consistent information in the Windows Registry, so in my current job I tended to audit this data directly via my scripts. This was saved into an SQL database from where it could be queried, or manipulated via a data source in Excel.

I’m working my notice period at the moment ready for a new job I’ll start in October, and so I’m going over the stuff I have created in the current job in order to prepare my handover documents. Mindful of the dependency my current employer has on these custom scripts I decided to get a quote for a Dell KACE solution, thinking that since it’s a Virtual Appliance, and since there are only 150 PCs here it shouldn’t be too expensive – after all it’s only really providing what my scripts already do (workstation builds, drivers, software deployment, and auditing). But here’s the thing – they wanted something like £13,000! (I can’t recall the precise figure). To put it in context this figure is around one third of the cost of replacing all the workstations with new ones, or say half the annual salary of an IT support technician – quite out of the question.

Unsurprisingly I have decided instead to simply tidy up my scripts to make them easier to use. Sure, you could accomplish these tasks with SCCM but that’s not free either. In an SME, why spend huge amounts of money on something that can be automated without much trouble using mechanisms that are built in. Heck, even the uninstall command line is stored in the registry for virtually all software – that’s how the Add/Remove Programs Control Panel works! And most software can be installed silently in the desired way provided you research the command line arguments to do so. It’s no accident that which was a great crowdsourced repository of this knowledge became KACE which was then acquired by Dell. It still exists, though the content doesn’t seem to be as well maintained as it was.

I have used a startup script written in VBScript to keep software up to date on workstations. A startup script runs as the SYSTEM account so permissions are not an issue. Since I also maintain an unattended installation I already have a package folder with all the scripts to install each package. All I needed to code was a way to audit the Registry for each package and add some logic around that. Up until now, I had tended to write sections of the script specifically tailored for each package, and from there it’s not much of a stretch to apply packages to a workstation based on its OS version, or Active Directory OU or group membership. For the script I have published below, I have recreated this logic as a single function which can be invoked with a one line entry for each package (see the highlighted part) – everything else is taken care of. I hope it helps someone to save £13,000 :)


Sample script output

Running software package check for Adobe Flash Player...
  Registry data found at branch "Adobe Flash Player ActiveX"
  Comparing detected version 11.3.300.271 against desired version 11.4.402.265
  Removing old version 11.3.300.271
    Killing iexplore.exe
    Override detected, running "u:\packages\flash\uninstall_flash_player.exe -uninstall"
    u:\packages\flash\uninstall_flash_player.exe -uninstall
  Installing Adobe Flash Player 11.4.402.265

Running software package check for Paint.NET...
  Registry data found at branch "{529125EF-E3AC-4B74-97E6-F688A7C0F1C0}"
  Comparing detected version 3.60.0 against desired version 3.60.0
  Paint.NET is already installed and up to date.

Running software package check for Adobe Reader...
  Registry data found at branch "{AC76BA86-7AD7-1033-7B44-AA0000000001}"
  Comparing detected version 10.0.0 against desired version 10.1.4
  Removing old version 10.0.0
    Using UninstallString from the Registry, plus "/qb-!"
    MsiExec.exe /I{AC76BA86-7AD7-1033-7B44-AA0000000001} /qb-!
  Installing Adobe Reader 10.1.4

Running software package check for Photo Gallery...
  Registry data found at branch "{60A1253C-2D51-4166-95C2-52E9CF4F8D64}"
  Comparing detected version 16.4.3503.0728 against desired version 16.4.3503.0728
  Photo Gallery is already installed and up to date.

Running software package check for Mendeley Desktop...
  Installing Mendeley Desktop 1.6

The script

'patters 2006-2012

Option Explicit
Dim objNetwork, objShell, objReg, strKey, colProcess, objProcess, arrSubKeys 
Dim strFileServer
Const HKEY_CURRENT_USER = &H80000001
Const HKEY_LOCAL_MACHINE = &H80000002

'set up objects
Set objNetwork = CreateObject("WScript.Network")
Set objShell = CreateObject("WScript.Shell")
Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")

strFileServer = "YOURSERVERHERE"
MapNetworkDrive "U:","unattended"

Package "flash.cmd", "Adobe Flash Player", "11.4.402.265", "u:\packages\flash\uninstall_flash_player.exe -uninstall", False, True, "iexplore.exe"    
Package "paintnet.cmd", "Paint.NET", "3.60.0", "/qb-!", False, False, "" 
Package "adobe.cmd", "Adobe Reader", "10.1.4","/qb-!",False, False, array("outlook.exe","iexplore")
Package "photogal.cmd", "Photo Gallery", "16.4.3503.0728", "/qb-!", False, False, "iexplore.exe"
Package "mendeley.cmd", "Mendeley Desktop", "1.6", "/S", True, False, "winword.exe"

objNetwork.RemoveNetworkDrive "U:", True, True
WScript.Echo VbCrLf & "Finished software checks"

Function Package(strPackageName, strTargetDisplayName, strTargetVersion, strExtraUninstParams, boolExtraUninstQuotes, boolUninstForceOverride, ProcessToKill)


  'To understand this function you need to know that installed software packages
  'will populate keys below these branches of the Registry:
  '  HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
  '  HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
  '    (the latter for 32bit software on 64bit Windows)
  'This is the data that is mined when you look at Add/Remove Programs
  'in the Control Panel 

  'strPackageName is the package script on your package server (e.g. flash.cmd)

  'strTargetDisplayName can be a full or partial match of the Registry key
  'DisplayName (matches from the left)
  '  "Java(TM)" would match "Java(TM) 6 Update 5" and all other versions

  'strTargetVersion is the full version number from DisplayVersion in the Registry
  'Each decimal point of precision will be compared in turn.

  'If the Registry key DisplayVersion is not used by a package, the same number
  'of digits is parsed from the right hand side of the DisplayName string

  'strExtraUninstParams is used when you want to override the command line
  'specified by QuietUninstallString in the Registry, or for when that value is
  'missing for example, sometimes InnoSetup packages will specify the switch
  '/SILENT in QuietUninstallString, but you may need to override by appending
  '/VERYSILENT to the command line in UninstallString
  'If neither QuietUninstallString and UninstallString are present, the script
  'will use strExtraUninstParams as the full uninstall command line
  'Some packages define UninstallString as a long filename but forget to
  'surround it with quotes. You can correct this by setting
  'boolExtraUninstQuotes = True
  '   Package "mendeley.cmd", "Mendeley Desktop", "1.6", "/S", True, False, "winword.exe"

  'In some cases you may want to ignore the value of both QuietUninstallString
  'and UninstallString and override the command completely. To do this, set
  'boolUninstForceOverride to True
  '   Package "flash.cmd", "Adobe Flash Player", "11.4.402.265", "u:\packages\flash\uninstall_flash_player.exe -uninstall", False, True, "iexplore.exe"

  'Finally, ProcessToKill is a string or array containing the name(s) of any
  'running process(es) you need to kill, if plugins are being installed for Word
  'or Internet Explorer for instance.


  Dim arrBranches, strBranch, boolRemoval, strActualDisplayName, strActualVersion
  Dim strQuietUninstall, strUninstall
  WScript.Echo VbCrLf & "Running software package check for " & strTargetDisplayName & "..."
  'we need to iterate through both the 32 and 64bit uninstall branches of the Registry
  arrBranches = Array("SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\", "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\")
  For Each strBranch In arrBranches
    'firstly, remove old version of package if it's present
    objReg.EnumKey HKEY_LOCAL_MACHINE, strBranch, arrSubKeys
    If IsArray(arrSubkeys) Then
      For Each strKey in arrSubkeys
        objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "DisplayName", strActualDisplayName
        If Left(strActualDisplayName, Len(strTargetDisplayName)) = strTargetDisplayName Then
          'we've found the target software package
          WScript.Echo "  Registry data found at branch """ & strKey & """"
          'is there a version string (not all software will have one)?
          objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "DisplayVersion", strActualVersion
          If Not IsNull(strActualVersion) Then
            'if there's no version string we'll try to grab the same number of chars from the right hand side of the DisplayName string  
            strActualVersion = Right(strActualDisplayName, Len(strTargetVersion))
          End If
          If (IsUpgradeNeeded (strActualVersion,strTargetVersion)) = True Then
            strQuietUninstall = ""
            WScript.Echo "  Removing old version " & strActualVersion
            KillProcess ProcessToKill
            'check the package's registry settings
            objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "UninstallString", strUninstall
            objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "QuietUninstallString", strQuietUninstall
            If Not strExtraUninstParams = "" Then
              'Extra parameters were sent to the function
              If boolUninstForceOverride = True Then
                'Entire uninstall command line was forced so use strExtraUninstParams, regardless of what's in the Registry
                WScript.Echo "    Override detected, running """ & strExtraUninstParams & """"
                WScript.Echo "    " & strExtraUninstParams
                WinExec strExtraUninstParams
              ElseIf Not IsNull(strUninstall) Then
                'use the basic UninstallString plus the additional parameters
                If boolExtraUninstQuotes = True Then
                  strUninstall = """" & strUninstall & """"
                End If
                strUninstall = strUninstall & " " & strExtraUninstParams
                WScript.Echo "    Using UninstallString from the Registry, plus """ & strExtraUninstParams & """"
                WScript.Echo "    " & strUninstall
                WinExec strUninstall
                'no UninstallString was found in the Registry, so assume that strExtraUninstParams is the full removal command line
                WScript.Echo "    No UninstallString found, running """ & strExtraUninstParams & """"
                WScript.Echo "    " & strExtraUninstParams
                WinExec strExtraUninstParams
              End If
              'No extra parameters were sent to the function
              'if there's already a value for QuietUninstallString then use that command line
              If Not IsNull(strQuietUninstall) Then
                WScript.Echo "    Using QuietUninstallString directly from the Registry"
                WScript.Echo "    " & strQuietUninstall
                WinExec strQuietUninstall
              ElseIf Not IsNull(strUninstall) Then
                'no QuietUninstallString was found, fall back to UninstallString
                If boolExtraUninstQuotes = True Then
                  strUninstall = """" & strUninstall & """"
                End If
                WScript.Echo "    Using UninstallString directly from the Registry"
                WScript.Echo "    " & strUninstall
                WinExec strUninstall
                WScript.Echo "    ERROR - this package doesn't seem to have any UninstallString defined - you'll need to send one to the Package function (see script source for details)"
                Exit Function
              End If
            End If
            'IsUpgradeNeeded (strActualVersion,strTargetVersion) is False
            'package was detected, but version is >= than the one specified
            WScript.Echo "  " & strTargetDisplayName & " is already installed and up to date."
            Exit Function
          End If
        End If
    End If
  'install package
  WScript.Echo "  Installing " & strTargetDisplayName & " " & strTargetVersion
  KillProcess ProcessToKill
  WinExec "U:\packages\" & strPackageName
End Function

Function IsUpgradeNeeded(strVerActual,strVerDesired)
  Dim arrActualVersion, arrDesiredVersion, i
  'Break software version down on decimal points
  arrActualVersion = split(strVerActual,".")
  arrDesiredVersion = split(strVerDesired,".")
  WScript.Echo "  Comparing detected version " & strVerActual & " against desired version " & strVerDesired
  'iterate, comparing each sub-version number starting from left
  For i = 0 To UBound(arrActualVersion)
    'WScript.Echo "  comparing digit... is " & arrActualVersion(i) & " less than " & arrDesiredVersion(i) 
    If arrActualVersion(i) < arrDesiredVersion(i) Then
      'installed version is out of date
      IsUpgradeNeeded = True
      Exit Function
    ElseIf arrActualVersion(i) > arrDesiredVersion(i) Then
      'installed version is newer
      IsUpgradeNeeded = False
      Exit Function     
    End If
  'thus far the version numbers are the same, but there may be additional
  'decimal points of precision in the desired version
  '  e.g. Adobe Reader 10.1.4 is newer than 10.1
  If UBound(arrDesiredVersion) > UBound(arrActualVersion) Then
    IsUpgradeNeeded = True
    IsUpgradeNeeded = False
  End If
End Function

Function MapNetworkDrive(strDriveLetter, strSharePath)
  On Error Resume Next
  'if the share name is not a UNC path, assume it's on the normal fileserver
  If Not Left(strSharePath,2) = "\\" Then
    strSharePath = "\\" & strFileServer & "\" & strSharePath
  End If
  If objFSO.DriveExists(strDriveLetter) Then
    objNetwork.RemoveNetworkDrive strDriveLetter, True, True
  End If
  objNetwork.MapNetworkDrive strDriveLetter, strSharePath
  If Err.Number <> 0 Then
    WScript.Echo "Error - " & Err.Description
  End If
  On Error Goto 0
End Function

Function WinExec(strExec)
  Dim objExec, eTime
  WinExec = True
  Set objExec = objShell.Exec(strExec)
  eTime = DateAdd("s", 120, Now)
  Do While objExec.Status = 0
    WScript.Sleep 1000
End Function

Function KillProcess(Process)
  Dim strProcessElement
  If IsArray(Process) Then
    For Each strProcessElement in Process
  ElseIf Not Process = "" Then
  End If
End Function

Function KillIndividualProcess(strProcess)
  Dim colProcess, objProcess
  Set colProcess = objWMI.ExecQuery("Select * from Win32_Process")
  For Each objProcess in colProcess
    If LCase(objProcess.Name) = LCase(strProcess) Then
      WScript.Echo "    Killing " & strProcess
      'occasionally one parent process may kill all children leading to an object error
      'so disable error handling temporarily
      On Error Resume Next
      On Error Goto 0
    End If
End Function

Building the Intel EMGD display driver for Sony VAIO P with fully working backlight control


The Windows 7 driver for the GMA 500 GPU has not been updated for nearly two years now (v2030 from September 2010). According to this document Intel will only support and continue to maintain the EMGD driver going forward. This is a driver for Linux and Windows primarily for embedded systems, but unfortunately its target audience is system manufacturers and not end users (it’s distributed as a driver build kit). You need quite a detailed technical understanding of the hardware you’re creating the driver for, in particular the LCD panel specifications. Sony are unlikely to provide new driver builds for a three year old laptop, and it will most likely be needed for Windows 8 compatibility. I seem to remember reading that the Windows 8 Release Preview will not accept the GMA 500 Windows 7 driver. The EMGD driver does also have one big advantage in that it includes an OpenGL ICD, which the Windows 7 GMA 500 driver has always lacked.

Thanks in part to ‘viewtiful’ on the Pocketables forum having shared the DTD details for the 1600×768 panel, several people (myself included) had built prior versions of EMGD for the VAIO P, but no one was able to get the LCD backlight working correctly. The onscreen control provided by Sony Shared Library has 8 different levels, and it would turn off the backlight completely at levels 1-3. Experimenting with building new drivers is an extremely slow and painstaking process, especially when you’re not very clear on which values may need tweaking, but I’m pleased to say that I finally got all 8 brightness levels working this evening. And rather than keeping that knowledge secret, I’m sharing it here so that other Vaio users can build their own EMGD drivers for future release versions.


Here is my pre-built driver:


Download and install Intel EMGD (version 1.16 from November 2012 is the latest at the time of writing). Launch the emgd-ced shortcut it has created on your desktop. This will start the java builder application.

Firstly create a new DTD called 1600×768@60Hz with settings as shown. Don’t worry about the greyed out values at the bottom of the screenshot – they’re not used.


Next create a new configuration called 16x7Sony like so:


Define the LVDS port name as MID (the name the regular GMA 500 driver uses), select the options as shown, taking care to select the custom DTD you just created:


In that same screen, click the Attributes button and set the Inverter Frequency to 300. Many thanks to Kirk over at the Intel Embedded Communities Forum for helping me to home in on this being the crucial setting. There are several mentions in the EMGD documentation of a reference value of 20300 which turns out to be incorrect for the VAIO P’s screen. I spent hours searching high and low in vain for a datasheet for this LCD panel (a Toshiba LT080EE04000). Eventually I discovered on the Notebook Review forum that a user called jeonghun had created an EMGD 1.10 build crucially with all eight backlight levels working for the VAIO X laptop which also has a GMA 500, though with a different 1366×768 panel. Since at this point I knew what to look for I opened his driver inf and discovered the magic value of 300. I took a guess that the motherboard-to-LCD circuitry would probably be similar for both models of VAIO.


Click Finish to close that window, and now click on Flat Panel Settings. The VAIO P panel is an 18bit panel. I can’t remember exactly, but I think all these values are the defaults:


Now that we’re finished with the LVDS settings (the built-in screen), click Next to move on to configuring the sDVO external monitor connection. Name it Monitor to keep it consistent with the Windows 7 GMA 500 driver. We don’t need any customization other than what is shown in this screenshot:


The final screen after this relates to building a video BIOS which we aren’t interested in so leave these settings on the defaults. Once finished, create a new package called SonyVaioP:


Finally while selecting the package, click on Generate Installation in the toolbar. This will create your zipped driver which can be found in:

EqualLogic, iSCSI and the Windows Server 2008 R2 firewall

I recently migrated a backup server from Windows Server 2003 to Windows 2008 R2 in order to install Backup Exec 2012 at the same time. Once I had configured everything I noticed in the iSCSI Control Panel that only one path would ever connect to the array, and I was getting regular iSCSI timeouts and failures in the System Event Log, which I hadn’t seen while running Windows 2003:


The errors were event 129:

The description for Event ID 129 from source iScsiPrt cannot be found. Either the component that raises this event is not installed on your local computer or the installation is corrupted. You can install or repair the component on the local computer.

event 39:

Initiator sent a task management command to reset the target. The target name is given in the dump data.

and event 9:

Target did not respond in time for a SCSI request. The CDB is given in the dump data.

Crucially these were spaced (all three together) at intervals of four minutes.


I spoke to the EqualLogic support team and, after a little while spent focusing on NIC drivers, one of the senior technicians fortunately realised that this four minute time interval coincides with the approximate frequency with which the array pings the initiators on the host and may send reconnect requests for additional path setup and load balancing. He recommended that I disable the Windows Firewall and sure enough the problem vanished. So it’s quite easy to inadvertently break iSCSI storage MPIO by making firewall settings changes to your system later on, and it’s easy to forget that these two things are related.

The problem for me was that this backup server has a NIC on the DMZ for faster backups (bypassing the hardware firewall). The pre- and post-backup job scripts enable and disable this NIC as required, but it does nonetheless need to be firewalled restrictively. In Windows 2003 the Windows Firewall can be enabled on a per NIC basis, however not in Windows 2008. Instead the firewall is configured instead in Network and Sharing Center on a per security zone basis (Domain Networks, Private Networks, Public Networks). The problem here is that the iSCSI NICs automatically end up in the Public zone, which is the most likely to be restricted. In my case, I had selected the option Block all connections including programs on the list of allowed programs. Even though the EqualLogic HIT Kit had specified an exemption rule, this was being denied.

Excluding iSCSI adapters

Relaxing the firewall in my scenario was not desirable, so I spent a while searching for a way to force the iSCSI NICs into the Private Networks zone. I couldn’t find one, though I did spot a method to exclude the NICs from the Network And Sharing Center altogether. In fact this same issue had been bothering people running VMware Workstation (because the VMware virtual NICs would get firewalled as Public Network connections), and fortunately someone had found a fix:

The solution posted there uses a PowerShell script which automatically targets VMware adapters, but we can use the same registry modification. So, on your server use Regedit to navigate to HKLM\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}. There is a child branch here for each NIC. Find your dedicated iSCSI NICs and for each one, create a new DWORD value called *NdisDeviceType (including the asterisk) and give it a value of 1. Now disable and re-enable each modified NIC. You will see that they disappear from Network and Sharing Center, and are now unaffected by the Windows Firewall.

By setting *NdisDeviceType to a value of 1 the NIC is designated as an endpoint device and is not considered to be connecting to an external network, which is probably quite appropriate for a dedicated iSCSI storage connection. I wonder whether this is the sort of thing that ought to be automated by the HIT kit in future in fact.

Preference Order

Another thing that’s easily overlooked on servers with iSCSI storage (because it’s so well hidden) is that if you have been changing NIC configs (changing drivers, adding hardware, P2V converting, etc.) then it’s quite likely that you may have affected the preference order in which network services use physical adapters. You don’t generally want the iSCSI ones to become the higher priority ones, and I have experienced strange issues with Exchange Server in the past owing to this, as well as licence issues with copy-protected software that relies on generating a unique hardware-dependent machine ID. To set the order, open Network and Sharing Center, then click on Change Adapter Settings on the left hand side. Now hold Alt, then Advanced -> Advanced Seettings. Now you can configure the LAN NICs with higher priority:


Corrupt Windows 7 NTFS junction points

I encountered an unusual problem recently – all Windows 7 workstations which had been built with a Microsoft Select Agreement Volume License version of Windows 7 Professional RTM using an unattended install, not via sysprep, had some sort of damage to their legacy filesystem junction points. This had prevented the installer for Kaspersky EndPoint Protection 8 and its Network Agent version 9 from running, though earlier versions had been fine. The error took Kaspersky support a very long time to pin down (several months in fact, despite them having detailed MSI installer logs), and it eventually transpired that many of the links to maintain legacy OS compatibility like C:\Documents and Settings -> C:\Users, or C:\Users\All Users -> C:\ProgramData on these affected systems were resolving to some kind of temporary mounted WIM image path, within the folder C:\Users\ADMINI~1\AppData\Local\Temp\mnt\wim.

This folder no longer existed, and nor was there any phantom mounted WIM image, so any attempt to access the damaged links would fail (in Kaspersky’s case the issue was C:\ProgramData\Application Data). I still have no idea what may have caused this. More recently the unattended install I designed uses Windows 7 Enterprise SP1, with no changes to the core build scripting, and systems built from this do not exhibit this issue. This might suggest it was a problem with Windows itself, and if so then my script to fix the damage could be useful for others.

The repair script requires SetACL.exe which is an extremely versatile tool, but which is syntactically very difficult to use! I compared the ACLs on a clean system, noted the link type (they’re not all junctions, there is one symlink), and whether or not there were deny permissions which prevent recursion on links which resolve to their parent folder e.g. C:\ProgramData\Application Data -> C:\ProgramData. The links are deleted and recreated, but only on systems that are detected to need the fix (see the highlighted line for that logic). If you set line 6 to “set DEBUG=echo” you can test the output before actually invoking the repair commands.

@echo off

:: Windows 7 junction point/symlink fix script
:: patters 13/03/2012

set DEBUG=

dir /aL C:\ProgramData | find /I "C:\Users\ADMINI~1\AppData\Local\Temp\mnt\wim\" && (
  call :junction /J "C:\Documents and Settings" "C:\Users" deny
  call :junction /J "C:\ProgramData\Application Data" "C:\ProgramData" deny
  call :junction /J "C:\ProgramData\Desktop" "C:\Users\Public\Desktop" deny
  call :junction /J "C:\ProgramData\Documents" "C:\Users\Public\Documents" deny
  call :junction /J "C:\ProgramData\Favorites" "C:\Users\Public\Favorites" deny
  call :junction /J "C:\ProgramData\Start Menu" "C:\ProgramData\Microsoft\Windows\Start Menu" nodeny
  call :junction /J "C:\ProgramData\Templates" "C:\ProgramData\Microsoft\Windows\Templates" deny
  call :junction /D "C:\Users\All Users" "C:\ProgramData" deny
  call :junction /J "C:\Users\All Users\Application Data" "C:\ProgramData" deny
  call :junction /J "C:\Users\All Users\Desktop" "C:\Users\Public\Desktop" deny
  call :junction /J "C:\Users\All Users\Documents" "C:\Users\Public\Documents" deny
  call :junction /J "C:\Users\All Users\Favorites" "C:\Users\Public\Favorites" deny
  call :junction /J "C:\Users\All Users\Start Menu" "C:\ProgramData\Microsoft\Windows\Start Menu" nodeny
  call :junction /J "C:\Users\All Users\Templates" "C:\ProgramData\Microsoft\Windows\Templates" deny
  call :junction /J "C:\Users\Public\Documents\My Music" "C:\Users\Public\Music" deny
  call :junction /J "C:\Users\Public\Documents\My Pictures" "C:\Users\Public\Pictures" deny
  call :junction /J "C:\Users\Public\Documents\My Videos" "C:\Users\Public\Videos" deny
  call :junction /J "C:\Users\Default User" "C:\Users\Default" deny
  call :junction /J "C:\Users\Default\Application Data" "C:\Users\Default\AppData\Roaming" deny
  call :junction /J "C:\Users\Default\Cookies" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Cookies" deny
  call :junction /J "C:\Users\Default\Local Settings" "C:\Users\Default\AppData\Local" deny
  call :junction /J "C:\Users\Default\My Documents" "C:\Users\Default\Documents" deny
  call :junction /J "C:\Users\Default\NetHood" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Network Shortcuts" deny
  call :junction /J "C:\Users\Default\PrintHood" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Printer Shortcuts" deny
  call :junction /J "C:\Users\Default\Recent" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Recent" deny
  call :junction /J "C:\Users\Default\SendTo" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\SendTo" deny
  call :junction /J "C:\Users\Default\Start Menu" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu" deny
  call :junction /J "C:\Users\Default\Templates" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Templates" deny
  call :junction /J "C:\Users\Default\Documents\My Music" "C:\Users\Default\Music" deny
  call :junction /J "C:\Users\Default\Documents\My Pictures" "C:\Users\Default\Pictures" deny
  call :junction /J "C:\Users\Default\Documents\My Videos" "C:\Users\Default\Videos" deny
  call :junction /J "C:\Users\Default\AppData\Local\Application Data" "C:\Users\Default\AppData\Local" deny
  call :junction /J "C:\Users\Default\AppData\Local\Temporary Internet Files" "C:\Users\Default\AppData\Local\Microsoft\Windows\Temporary Internet Files" deny
) || echo Legacy filesystem junction points/symlinks are fine.

::odd permissions for this one, so I'm leaving it out
::call :junction /J "C:\Users\Default\AppData\Local\History" "C:\Users\Default\AppData\Local\Microsoft\Windows\History" deny

goto :eof

:: %1 = type (junction or directory symlink)
:: %2 = junction/symlink path
:: %3 = target path
:: %4 = set the deny permission or not

::delete old junction point
%DEBUG% rmdir "%~2"

::create new junction point
%DEBUG% mklink %1 "%~2" "%~3"

::set owner to SYSTEM
%DEBUG% setacl -on "%~2" -ot file -actn setowner -ownr "n:SYSTEM"

:: we need to stop inheritance of permissions before we make changes. This must be done with
:: a separate commandline entry owing to the order in which SetACL.exe processes its arguments.
%DEBUG% setacl -on "%~2" -ot file -actn setprot -op "dacl:p_c;sacl:p_c"

::clear ACL and set permissions
%DEBUG% setacl -on "%~2" -ot file -actn clear -clr "dacl,sacl" -actn ace -ace "n:Everyone;i:np;p:read_ex" -actn ace -ace "n:SYSTEM;i:np;p:full" -actn ace -ace "n:Administrators;i:np;p:full"

::add directory listing deny permission for recursive paths if needed
if "%4"=="deny" %DEBUG% setacl -on "%~2" -ot file -actn ace -ace "n:Everyone;s:n;m:deny;i:np;p:list_dir"