Tag Archives: VBScript

Windows 7 new user profile – MSOE.DLL could not be loaded error

Since my Windows 7 deployment I get an error every time a new user logs in (or logs in following a profile reset).

Windows Mail could not be started because MSOE.DLL could not be loaded

Since you can click past it I wasn’t initially too concerned but I soon came to realize that there’s pretty much no information on the Web about this problem. Anything the search engines turn up is invariably about Outlook Express on Windows 98/Me.

It seems to be caused when some kind of IE First Run process opens Internet Explorer with an MSN page and presumably tries to register the default email client (which is not yet set to Outlook, despite it being installed). The strange thing is that Windows Mail is not even available on Windows 7. Microsoft discontinued it after Vista in favour of Windows Live Mail. WinMail.exe is present on the system but it’s hidden, and that MSOE.DLL has been purposely omitted. Apparently you can get it working again by supplying a copy from a Vista PC.

The problem is that this error looks important enough for a new user to contact the helpdesk about. The other issue is that it significantly delays that first logon, sometimes by several entire minutes. The error occurs immediately after you enter your credentials (you can hear the alert sound) but it happens while the desktop is still hidden from view by the Windows 7 splash screen. Eventually the Preparing your desktop splash screen times out and you can click OK:

Windows Mail could not be started because MSOE.DLL could not be loaded

Notice the label in that floating window – it’s setting up a component which is titled as Microsoft Windows.

Eventually I discovered a KB article relating to Internet Explorer 5 personalized settings, which was when Microsoft introduced this method of installation.

Having consulted that document I viewed HKLM\SOFTWARE\Microsoft\ActiveSetup\InstalledComponents in Regedit and saw that there is indeed a component with the name “Microsoft Windows” which is invoking WinMail.exe, intending to register it as a mail and news (nntp) client:

WinMail.exe ActiveSetup parameters

I have no idea why it’s doing this since WinMail.exe is intentionally disabled in Windows 7. It would seem logical that deleting this whole key should fix the problem (I backed it up first of course). In testing this didn’t seem to work though.

However I did notice that sometimes I got the error on screen twice and other times only once. Exasperated I searched the registry for instances of WinMail.exe until I discovered the reason: the entire ActiveSetup branch of the registry also exists under HKLM\Software\WOW6432Node\Microsoft, for the 32bit version of Internet Explorer on 64bit systems. Once both keys had been deleted the error disappeared.

I was then able to refine this – instead of deleting the whole key you only need to set to the single DWORD value IsInstalled to zero. Do this to both instances and the problem is fixed! In my environment I used my existing VBScript startup script which worktations inherit by Group Policy. Excerpt below:

If InStr (strOS,"Microsoft Windows 7") Then
  'Prevent Windows from trying to invoke the non-existent Windows Mail for new user profile setup
  On Error Resume Next
  strKey = "SOFTWARE\Microsoft\Active Setup\Installed Components\{44BBA840-CC51-11CF-AAFA-00AA00B6015C}"
  objReg.SetDWORDValue HKEY_LOCAL_MACHINE,strKey,"IsInstalled",0
  strKey = "SOFTWARE\Wow6432Node\Microsoft\Active Setup\Installed Components\{44BBA840-CC51-11CF-AAFA-00AA00B6015C}"
  objReg.SetDWORDValue HKEY_LOCAL_MACHINE,strKey,"IsInstalled",0
  On Error Goto 0

Enabling Network Level Authentication on Windows XP by script

Migrating to Windows 7 has thrown up another problem – users wanting to connect from home computers running XP cannot use the Remote Desktop Client to connect to their newly upgraded office PCs. The Network Level Authentication change to the Remote Desktop Client was made because the original RDP is susceptible to Man-in-the-middle attacks.

Rather than leaving the new systems vulnerable by allowing connections from all clients in Computer Propertes > Remote settings, I discovered that Windows XP SP3 does in fact offer NLA support however it’s disabled by default. Somewhat frustratingly, the steps outlined in Microsoft KB 951608 require Registry edits which I would not want to encourage non-IT-savvy people to try. Giving out a .reg file is not really a good idea here either since these are additions to existing values, so forced replacements could interfere with certain vendors’ VPN clients etc.

Here’s a VBScript for the task which will only install on XP SP3 and will detect if the modifications have already been made. You could easily target it at a whole group of PCs by iterating through an array of hostnames.

'Enables Network Level Authentication on XP SP3 (disabled by default)
'which allows you to use the Remote Desktop Client 6.1 to connect to
'Windows 7 and Windows Server 2008 R2 without degrading security

Option Explicit

Const HKEY_LOCAL_MACHINE = &H80000002

Dim strLsaKey, strLsaValue, strHostname, size, arrMultiRegSZ, objReg, objWMI, colItems, i, found, modified
Dim objItem, SPlevel, strOSVer, strSecProvKey, strSecProvValue, strValue

strLsaKey = "SYSTEM\CurrentControlSet\Control\Lsa"
strLsaValue = "Security Packages"
strSecProvKey = "SYSTEM\CurrentControlSet\Control\SecurityProviders"
strSecProvValue = "SecurityProviders"
strHostname = "."
modified = false
found = false

Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strHostname & "\root\cimv2")
Set colItems = objWMI.ExecQuery("SELECT * FROM Win32_OperatingSystem")
For Each objItem In colItems
  strOSVer = objItem.Version
  SPlevel = objItem.ServicePackMajorVersion
Next
If Not Left(strOSVer,3) = "5.1" Then
  WScript.Echo "This script is only intended for Windows XP."
  WScript.Quit
End If
If Not SPlevel >= 3 Then
  WScript.Echo "Please install the latest Windows XP Service Pack from Windows Update."
  WScript.Quit
End If

Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strHostname & "\root\default:StdRegProv")
objReg.GetMultiStringValue HKEY_LOCAL_MACHINE, strLsaKey, strLsaValue, arrMultiRegSZ
size = Ubound(arrMultiRegSZ)
For i=0 to size
  If arrMultiRegSZ(i) = "tspkg" Then
    found = true
  End If
Next
If found Then
  WScript.Echo "tspkg already added to HKLM\SYSTEM\CurrentControlSet\Control\Lsa"
Else
  ReDim Preserve arrMultiRegSZ(size + 1)
  arrMultiRegSZ(size + 1) = "tspkg"
  objReg.SetMultiStringValue HKEY_LOCAL_MACHINE, strLsaKey, strLsaValue, arrMultiRegSZ
  modified = true
End If

objReg.GetStringValue HKEY_LOCAL_MACHINE, strSecProvKey, strSecProvValue, strValue

If Instr(strValue,"credssp.dll") Then
  WScript.Echo "credssp.dll already added to HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders"
Else
  strValue = strValue & ", credssp.dll"
  objReg.SetStringValue HKEY_LOCAL_MACHINE, strSecProvKey, strSecProvValue, strValue
  modified = true
End If
If modified Then
  WScript.Echo "Settings updated. You will need to restart for the changes to become active."
End If

Set objReg = nothing
Set objWMI = nothing

Customizing Windows 7 unattend.xml

Windows 7, like Vista, uses an XML answer file to configure the OS install. What’s neat about this is that even though you use the WAIK‘s WSIM tool to edit and validate it, you can customize it and add your own sections for software packages etc. as you can see from the example below, though these custom sections will need to be inserted after the sections that WSIM validates. This answer file can easily be parsed with VBScript using MSXML DOM, allowing for variables like passwords, driver sets, product keys and so on to be inserted at build time.

Why use an unattended install?

If you’ve always used an unattended install to build your workstations, you’ll know that they can be extremely versatile. If you already have a scripted build for XP with applications then chances are you’ll want to tweak those scripts to work with Windows 7. Sure, Sysprep images are handy too, but unless all your machines are the same, or all your packaged app requirements are identical, then you’ll need to add a load of scripted customization to them anyway. Which begs the question: why not just use an unattended install? That way you eliminate application problems that sometimes surface after image-based deployment. In a previous job I remember Roxio deployment in particular was a nightmare for this reason.

Microsoft have certainly made things considerably easier with the release of the Microsoft Deployment Toolkit 2010 but, though it offers a great introduction into the process of automated system building, it lacks the flexibility of rolling your own build process – in particular if you already have a host database. When I decide to rebuild a machine, it boots Windows PE from WDS, looks up its MAC address in the host database and reads the model type from WMI and will offer default choices in my build script menus based on that. It also works out which is the nearest site file server to use for the install.

The problem with Sysprep deployment if you have a very mixed hardware environment is that you either have to:

  • Create a WIM image for each different hardware type (lots of boring maintenance when changes are required)
  • Add a huge bulk of drivers to a single WIM image with DISM
  • Use the AuditSystem phase to connect to a driver share and re-detect all the hardware

Since Windows Vista and later versions effectively just install a WIM and run a hardware detection phase during their normal install process, Sysprep no longer offers much of a speed improvement over an unattended build.

Device drivers

Additional mass storage and networking drivers that will be essential during setup are detected in the Windows PE instance (which we booted from WDS) Driver Store and ‘reflected’ into the installed OS. As you build your custom Windows PE boot image, add these using DSIM as described in my post on the subject.

Since we can read the PC model name from the BIOS via WMI, we can add a tailored device path to the offlineServicing phase of setup. This allows for easy maintenance of driver bundles, since we can arrange them by model type (which certainly beats having dozens of SoundMAX audio drivers lumped together), and we can limit which drivers are offered to each model – particularly useful when a driver causes problems for some models, as with the GMA950 driver and Adobe Flash on Intel 945G motherboards.

I noticed that this is in fact considerably neater that the method used in MDT2010’s ZTIDrivers.wsf script, which copies drivers to the local system then invokes the AuditSystem phase, running the PnP detection routines a second time, slowing down the install. Useful for an OEM like Dell I suppose, whose PCs are often started from their factory image with no connectivity, but not ideal for corporate LANs.

Sample autounattend.xml with custom sections at the end

Note 1 – I couldn’t find any examples of this online, but I discovered that the values for pre-populating the Internet Explorer 8 Search Providers can be obtained by configuring a workstation, then harvesting the registry settings from HKCU\Software\Microsoft\Internet Explorer\SearchScopes. I have highlighted the relevant lines in the XML below.

Note 2 – Somewhat confusingly, in the offlineServicing phase Microsoft-Windows-PnpCustomizationsNonWinPE will fail to connect to your file server for drivers unless you connect to its FQDN (assuming the unattended launched the OS build from a share referencing just the NetBIOS name). Fail to do this and %systemroot%\panther\setupact.log will reveal that it fails to connect with error 0x4C3 (multiple credentials on connection to the same server). What’s bizarre is that there certainly aren’t multiple credentials in use – I use the same ones throughout. I wrote up this problem and solution in this thread on the MSFN Forums. I suspect this might be caused because I launch setup.exe from a network drive rather than mounting the OS WIM image from a WDS server (I wanted to maintain consistency with my other legacy OS builds). I have highlighted this on line 57.

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
        <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SetupUILanguage>
                <UILanguage>en-US</UILanguage>
            </SetupUILanguage>
            <InputLocale>0809:00000809</InputLocale>
            <SystemLocale>en-GB</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-GB</UserLocale>
        </component>
        <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComplianceCheck>
                <DisplayReport>Never</DisplayReport>
            </ComplianceCheck>
            <Diagnostics>
                <OptIn>false</OptIn>
            </Diagnostics>
            <DiskConfiguration>
                <WillShowUI>Always</WillShowUI>
            </DiskConfiguration>
            <DynamicUpdate>
                <Enable>true</Enable>
                <WillShowUI>OnError</WillShowUI>
            </DynamicUpdate>
            <ImageInstall>
                <OSImage>
                    <InstallFrom>
                        <MetaData wcm:action="add">
                            <Key>/IMAGE/NAME</Key>
                            <Value>Windows 7 PROFESSIONAL</Value>
                        </MetaData>
                    </InstallFrom>
                </OSImage>
            </ImageInstall>
            <UserData>
                <AcceptEula>true</AcceptEula>
                <FullName>IT</FullName>
                <Organization>My company</Organization>
                <ProductKey>
                    <WillShowUI>OnError</WillShowUI>
                </ProductKey>
            </UserData>
            <EnableNetwork>true</EnableNetwork>
        </component>
    </settings>
    <settings pass="offlineServicing">
        <component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <DriverPaths>
                <PathAndCredentials wcm:action="add" wcm:keyValue="common">
                    <Credentials>
                        <Domain>domain.com</Domain>
                        <Password>Password</Password>
                        <Username>unattended</Username>
                    </Credentials>
                    <Path>\\myserver.domain.com\UNATTENDED\drivers\7-x64\common</Path>
                </PathAndCredentials>
                <PathAndCredentials wcm:action="add" wcm:keyValue="build">
                    <Credentials>
                        <Domain>*value to be set by install.vbs*</Domain>
                        <Password>*value to be set by install.vbs*</Password>
                        <Username>*value to be set by install.vbs*</Username>
                    </Credentials>
                    <Path>*value to be set by install.vbs*</Path>
                </PathAndCredentials>
            </DriverPaths>
        </component>
    </settings>
    <settings pass="generalize">
        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>
        </component>
    </settings>
    <settings pass="specialize">
        <component name="Microsoft-Windows-IE-InternetExplorer" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Home_Page>http://mycompany.com</Home_Page>
            <CompanyName>My Company</CompanyName>
            <FavoritesOnTop>true</FavoritesOnTop>
            <FilterLevel>High</FilterLevel>
            <Help_Page></Help_Page>
            <DisableFirstRunWizard>true</DisableFirstRunWizard>
            <DisableWelcomePage>true</DisableWelcomePage>
            <PlaySound>true</PlaySound>
            <ShowInformationBar>true</ShowInformationBar>
            <UserAgent></UserAgent>
            <Window_Title_CN></Window_Title_CN>
            <SearchScopes>
                <Scope wcm:action="add">
                    <ScopeDefault>true</ScopeDefault>
                    <ScopeKey>Search1</ScopeKey>
                    <ScopeDisplayName>Google</ScopeDisplayName>
                    <ScopeUrl>http://www.google.com/search?q={searchTerms}&amp;sourceid=ie7&amp;rls=com.microsoft:{language}:{referrer:source}&amp;ie={inputEncoding?}&amp;oe={outputEncoding?}</ScopeUrl>
                    <FaviconURL>http://www.google.com/favicon.ico</FaviconURL>
                    <SuggestionsURL>http://clients5.google.com/complete/search?q={searchTerms}&amp;client=ie8&amp;mw={ie:maxWidth}&amp;sh={ie:sectionHeight}&amp;rh={ie:rowHeight}&amp;inputencoding={inputEncoding}&amp;outputencoding={outputEncoding}</SuggestionsURL>
                </Scope>
                <Scope wcm:action="add">
                    <ScopeKey>Search2</ScopeKey>
                    <ScopeDisplayName>Bing</ScopeDisplayName>
                    <FaviconURL>http://www.bing.com/favicon.ico</FaviconURL>
                    <ScopeUrl>http://www.bing.com/search?q={searchTerms}&amp;form=IE8SRC&amp;src=IE-SearchBox</ScopeUrl>
                    <SuggestionsURL>http://api.bing.com/qsml.aspx?query={searchTerms}&amp;market={Language}&amp;form=IE8SSC&amp;maxwidth={ie:maxWidth}&amp;rowheight={ie:rowHeight}&amp;sectionHeight={ie:sectionHeight}</SuggestionsURL>
                </Scope>
            </SearchScopes>
            <EnableLinksBar>false</EnableLinksBar>
            <PrintBackground>true</PrintBackground>
        </component>
        <component name="Microsoft-Windows-RemoteAssistance-Exe" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <fAllowFullControl>true</fAllowFullControl>
            <fAllowToGetHelp>true</fAllowToGetHelp>
        </component>
        <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipAutoActivation>true</SkipAutoActivation>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>*</ComputerName>
            <ProductKey>XXXXX-YYYYY-ZZZZZ-YYYYY-XXXXX</ProductKey>
            <RegisteredOrganization>My Company</RegisteredOrganization>
            <RegisteredOwner>IT</RegisteredOwner>
        </component>
        <component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <fDenyTSConnections>false</fDenyTSConnections>
        </component>
        <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Identification>
                <Credentials>
                    <Domain>*value to be set by install.vbs*</Domain>
                    <Password>*value to be set by install.vbs*</Password>
                    <Username>*value to be set by install.vbs*</Username>
                </Credentials>
                <JoinDomain>domain.com</JoinDomain>
                <MachineObjectOU>OU=Windows 7,OU=Workstations,DC=domain,DC=com</MachineObjectOU>
            </Identification>
        </component>
        <component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <DomainProfile_EnableFirewall>false</DomainProfile_EnableFirewall>
        </component>
        <component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <UserAuthentication>1</UserAuthentication>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SystemLocale>en-GB</SystemLocale>
            <UILanguage>en-GB</UILanguage>
            <UserLocale>0809:00000809</UserLocale>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <RegisteredOrganization>My Company</RegisteredOrganization>
            <RegisteredOwner>IT</RegisteredOwner>
            <TimeZone>GMT Standard Time</TimeZone>
            <OEMInformation>
                <Manufacturer>*</Manufacturer>
                <Model>*</Model>
            </OEMInformation>
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>1</ProtectYourPC>
            </OOBE>
            <UserAccounts>
                <AdministratorPassword>
                    <Value>MwBifhgftytredjghAcwB0AHIAYQ584jkhkgtEAcwBzAHcAbwByAGQA</Value>
                    <PlainText>false</PlainText>
                </AdministratorPassword>
                <LocalAccounts>
                    <LocalAccount wcm:action="add">
                        <Password>
                            <Value>QQBTAEUANABoAGchgfhgfd357wrysAHMAcwB3AG8AcgBkAA==</Value>
                            <PlainText>false</PlainText>
                        </Password>
                        <Name>cust_localuser</Name>
                        <DisplayName>cust_localuser</DisplayName>
                        <Description>Dummy user required for unattended - delete later</Description>
                        <Group>users</Group>
                    </LocalAccount>
                </LocalAccounts>
            </UserAccounts>
            <VisualEffects>
                <FontSmoothing>ClearType</FontSmoothing>
            </VisualEffects>
            <FirstLogonCommands>
                <SynchronousCommand wcm:action="add">
                    <Order>1</Order>
                    <Description>Connect to unattended share</Description>
                    <CommandLine>net use *value to be set by install.vbs*</CommandLine>
                </SynchronousCommand>
                <SynchronousCommand wcm:action="add">
                    <Order>2</Order>
                    <Description>Launch package installer</Description>
                    <CommandLine>cscript *value to be set by install.vbs*</CommandLine>
                    <RequiresUserInput>true</RequiresUserInput>
                </SynchronousCommand>
            </FirstLogonCommands>
            <AutoLogon>
                <Password>
                    <Value>MwBifhgftytredjghAcwB0AHIAYQ584jkhkgtEAcwBzAHcAbwByAGQA</Value>
                    <PlainText>false</PlainText>
                </Password>
                <Username>Administrator</Username>
                <LogonCount>1</LogonCount>
                <Enabled>true</Enabled>
            </AutoLogon>
        </component>
    </settings>
    <cpi:offlineImage cpi:source="wim://myserver/unattended/os/7-x64/sources/install.wim#Windows 7 PROFESSIONAL" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
    <!-- MY COMPANY CUSTOMIZATIONS BELOW-->
    <mycompany:custom xmlns:mycompany="urn:schemas-domain-com:mycompany">
        <!-- build description -->
        <cust_description>standard workstation</cust_description>
        <!-- list of packages to install in order. Descriptions are read from U:\scripts\packages.csv -->
        <cust_packages>office2k7.cmd,adobe.cmd,flash.cmd,java.cmd,fonts.cmd</cust_packages>
        <!-- start of the model name string, as reported by WMI, comma separated - allows auto-selection of build. -->
        <cust_models>OptiPlex 745,OptiPlex GX620,VMware Virtual Platform</cust_models>
        <!-- do we want packages.vbs to activate Windows? -->
        <cust_activate>false</cust_activate>
        <!-- do we want a hibernation file wasting disk space? If set to false packages.vbs will disable hibernation support -->
        <cust_hibernate>false</cust_hibernate>
    </mycompany:custom>
</unattend>
 

Sample VBScript code for parsing the XML using MSXML DOM

This is not a complete script – it’s intended purely to illustrate the concept.

'New OS - 7/Vista/2008/2008R2

'read the answer file
Set objXML = CreateObject("Microsoft.XMLDOM")
objXML.Async = "False"
objXML.Load(strMedia & "\sifs\" & strOS & "\" & strBuild & ".xml")

'Additional mass storage and Networking drivers that will be essential for the rest of the build are detected in the
'running Windows PE instance's Driver Store and reflected into the installed OS

'insert the non-WinPE PnP driver discovery paths and credentials for the offlineServicing pass
strXPath = "/unattend/settings[@pass='offlineServicing']/component[@name='Microsoft-Windows-PnpCustomizationsNonWinPE']/DriverPaths/PathAndCredentials/Credentials"
'we need to iterate since there may be several sets of credentials (one for each driver path)
Set colNodes=objXML.selectNodes(strXPath & "/Domain")
For Each objNode In colNodes
  objNode.Text = strDomainName
Next
Set colNodes=objXML.selectNodes(strXPath & "/Password")
For Each objNode In colNodes
  objNode.Text = strPass
Next
Set colNodes=objXML.selectNodes(strXPath & "/Username")
For Each objNode In colNodes
  objNode.Text = strUser  
Next

'Insert hostname
strXPath = "/unattend/settings[@pass='specialize']/component[@name='Microsoft-Windows-Shell-Setup']/ComputerName"
Set objNode=objXML.selectSingleNode(strXPath)
objNode.Text = strComputerName

'insert credentials for Domain join
On Error Resume Next
strXPath = "/unattend/settings[@pass='specialize']/component[@name='Microsoft-Windows-UnattendedJoin']/Identification/Credentials"
Set objNode=objXML.selectSingleNode(strXPath & "/Domain")
If Err.Number = 0 Then
  'Microsoft-Windows-UnattendedJoin exists - carry on modifying (workgroup builds won't have this section in the XML)
  objNode.Text = strDomainName
  Set objNode=objXML.selectSingleNode(strXPath & "/Password")
  objNode.Text = strPass
  Set objNode=objXML.selectSingleNode(strXPath & "/Username") 
  objNode.Text = strUser
End If
On Error Goto 0

'insert manufacturer and model into OEMinfo
strXPath = "/unattend/settings[@pass='oobeSystem']/component[@name='Microsoft-Windows-Shell-Setup']/OEMInformation/Manufacturer"
Set objNode=objXML.selectSingleNode(strXPath)
objNode.Text = strManufacturer
strXPath = "/unattend/settings[@pass='oobeSystem']/component[@name='Microsoft-Windows-Shell-Setup']/OEMInformation/Model"
Set objNode=objXML.selectSingleNode(strXPath)
objNode.Text = strModel & " - Your Company"

strXPath = "/unattend/settings[@pass='oobeSystem']/component[@name='Microsoft-Windows-Shell-Setup']/FirstLogonCommands/SynchronousCommand/CommandLine"
Set colNodes=objXML.selectNodes(strXPath)
For Each objNode In ColNodes
  'Insert the unattended share info and credentials
  If InStr (objNode.Text,"net use *value to be set by install.vbs*") Then
    objNode.Text = "net use " & strMedia & " \\" & strInstallServer & "\" & strShare & " /user:" & strUser & "@" & strDomainName & " " & strPass & " /persistent:no"
  End If
  'Record our build file selection for the package installer later
  If InStr (objNode.Text,"cscript *value to be set by install.vbs*") Then
    objNode.Text = "cscript //nologo " & strMedia & "\scripts\packages.vbs " & strMedia & " " & strOS & " " & strBuild & ".xml"
  End If
Next

Set objNode = Nothing
'write out the answer file
objXML.Save "x:\autounattend.xml" 
Set objXML = Nothing

Outlook stationery – scripting default font and HTML signature

Many organizations decide to use a house style for email, and being able to force a default font is the only reliable way to get this consistent. I do this with the login script.

What’s difficult about it is that it’s not just a minor registry edit – the font styles are actually written in HTML and then stored in binary in the registry. The best way to get them modified is to use an Outlook client to make the changes, then grab them from the registry. They’re stored at:

HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Common\MailSettings

In Outlook you need to edit the stationery in Tools > Options > Mail Format tab > Stationery and Fonts, making sure to change the font for both new messages and replies.

The registry values which are modified are:

  • ComposeFontComplex
  • ComposeFontSimple
  • ReplyFontComplex
  • ReplyFontSimple

Here’s what my ComposeFontComplex looks like converted back to text:

<html>

<head>
<style>

 /* Style Definitions */
 span.PersonalComposeStyle
	{mso-style-name:"Personal Compose Style";
	mso-style-type:personal-compose;
	mso-style-noshow:yes;
	mso-style-unhide:no;
	mso-ansi-font-size:10.0pt;
	mso-bidi-font-size:11.0pt;
	font-family:"Verdana","sans-serif";
	mso-ascii-font-family:Verdana;
	mso-hansi-font-family:Verdana;
	mso-bidi-font-family:"Times New Roman";
	mso-bidi-theme-font:minor-bidi;
	color:windowtext;}
-->
</style>
</head>

</html>

The cleanest way to script this is to export the values from regedit (they will be in hex) and insert them into the script unmodified with the line breaks intact. Then you just quote and line-wrap them, split them into an array on the ”,” character and use a hex-to-binary function before inserting them into the user registry. Here is my example OutlookStationery subroutine for reference  (note that it’s not a working script – some objects are defined elsewhere, e.g. objReg, the Active Directory job title, name, surname etc.):

Sub OutlookStationery
  'Standardized Outlook signature based on AD data
  WScript.Echo "Updating Outlook signature and default font"
  Dim objWord, objDoc, objSelection, objEmailOptions, objSignatureObject, objSignatureEntries, objLink
  Set objWord = CreateObject("Word.Application")
  Set objDoc = objWord.Documents.Add()
  Set objSelection = objWord.Selection
  Set objEmailOptions = objWord.EmailOptions
  Set objSignatureObject = objEmailOptions.EmailSignature
  Set objSignatureEntries = objSignatureObject.EmailSignatureEntries
  objSelection.TypeText strFirstName & " " & strSurname
  objSelection.TypeText(Chr(11))
  objSelection.TypeText strTitle
  objSelection.TypeText(Chr(11))
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Tel +xx xxx xxxx " & strExtension
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Fax +xx xxx xxxx xxxx"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Web "
  Set objLink = objSelection.Hyperlinks.Add(objSelection.Range,"http://mycompanywebsite.com/",,"My Company Website","companywebsite.com")
  objSelection.TypeText(Chr(11))
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "The Company Name"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Address Line 1"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Address City"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText(Chr(11))
  objselection.Font.Bold = True
  objSelection.TypeText "Company"
  objselection.Font.Bold = False
  objSelection.TypeText " strapline goes here"
  Set objSelection = objDoc.Range()
  objSelection.Font.Name = "Verdana"
  objSelection.Font.Size = "10"
  objSignatureEntries.Add "Standard Signature", objSelection
  objSignatureObject.NewMessageSignature = "Standard Signature"
  objSignatureObject.ReplyMessageSignature = ""
  objDoc.Saved = True
  objWord.Quit

  'Force the default font for Outlook messages. The hex arrays are captures from a Regedit export, line breaks intact for easy later amendment
  Dim arrComposeFontComplexHex, arrComposeFontComplex, arrReplyFontComplexHex, arrReplyFontComplex
  Dim arrComposeFontSimpleHex, arrComposeFontSimple, arrReplyFontSimpleHex, arrReplyFontSimple

  arrComposeFontComplexHex = Split ("3c,68,74,6d,6c,3e,0d,0a,0d,0a,3c,68,65,61,64,3e,0d,0a," &_
   "3c,73,74,79,6c,65,3e,0d,0a,0d,0a,20,2f,2a,20,53,74,79,6c,65,20,44,65,66,69," &_
   "6e,69,74,69,6f,6e,73,20,2a,2f,0d,0a,20,73,70,61,6e,2e,50,65,72,73,6f,6e,61," &_
   "6c,43,6f,6d,70,6f,73,65,53,74,79,6c,65,0d,0a,09,7b,6d,73,6f,2d,73,74,79,6c," &_
   "65,2d,6e,61,6d,65,3a,22,50,65,72,73,6f,6e,61,6c,20,43,6f,6d,70,6f,73,65,20," &_
   "53,74,79,6c,65,22,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,74,79,70,65,3a," &_
   "70,65,72,73,6f,6e,61,6c,2d,63,6f,6d,70,6f,73,65,3b,0d,0a,09,6d,73,6f,2d,73," &_
   "74,79,6c,65,2d,6e,6f,73,68,6f,77,3a,79,65,73,3b,0d,0a,09,6d,73,6f,2d,73,74," &_
   "79,6c,65,2d,75,6e,68,69,64,65,3a,6e,6f,3b,0d,0a,09,6d,73,6f,2d,61,6e,73,69," &_
   "2d,66,6f,6e,74,2d,73,69,7a,65,3a,31,30,2e,30,70,74,3b,0d,0a,09,6d,73,6f,2d," &_
   "62,69,64,69,2d,66,6f,6e,74,2d,73,69,7a,65,3a,31,31,2e,30,70,74,3b,0d,0a,09," &_
   "66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,22,56,65,72,64,61,6e,61,22,2c,22,73,61," &_
   "6e,73,2d,73,65,72,69,66,22,3b,0d,0a,09,6d,73,6f,2d,61,73,63,69,69,2d,66,6f," &_
   "6e,74,2d,66,61,6d,69,6c,79,3a,56,65,72,64,61,6e,61,3b,0d,0a,09,6d,73,6f,2d," &_
   "68,61,6e,73,69,2d,66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,56,65,72,64,61,6e,61," &_
   "3b,0d,0a,09,6d,73,6f,2d,62,69,64,69,2d,66,6f,6e,74,2d,66,61,6d,69,6c,79,3a," &_
   "22,54,69,6d,65,73,20,4e,65,77,20,52,6f,6d,61,6e,22,3b,0d,0a,09,6d,73,6f,2d," &_
   "62,69,64,69,2d,74,68,65,6d,65,2d,66,6f,6e,74,3a,6d,69,6e,6f,72,2d,62,69,64," &_
   "69,3b,0d,0a,09,63,6f,6c,6f,72,3a,77,69,6e,64,6f,77,74,65,78,74,3b,7d,0d,0a," &_
   "2d,2d,3e,0d,0a,3c,2f,73,74,79,6c,65,3e,0d,0a,3c,2f,68,65,61,64,3e,0d,0a,0d," &_
   "0a,3c,2f,68,74,6d,6c,3e,0d,0a", ",")

  arrReplyFontComplexHex = Split ("3c,68,74,6d,6c,3e,0d,0a,0d,0a,3c,68,65,61,64,3e,0d,0a," &_
   "3c,73,74,79,6c,65,3e,0d,0a,0d,0a,20,2f,2a,20,53,74,79,6c,65,20,44,65,66,69," &_
   "6e,69,74,69,6f,6e,73,20,2a,2f,0d,0a,20,73,70,61,6e,2e,50,65,72,73,6f,6e,61," &_
   "6c,52,65,70,6c,79,53,74,79,6c,65,0d,0a,09,7b,6d,73,6f,2d,73,74,79,6c,65,2d," &_
   "6e,61,6d,65,3a,22,50,65,72,73,6f,6e,61,6c,20,52,65,70,6c,79,20,53,74,79,6c," &_
   "65,22,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,74,79,70,65,3a,70,65,72,73," &_
   "6f,6e,61,6c,2d,72,65,70,6c,79,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,6e," &_
   "6f,73,68,6f,77,3a,79,65,73,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,75,6e," &_
   "68,69,64,65,3a,6e,6f,3b,0d,0a,09,6d,73,6f,2d,61,6e,73,69,2d,66,6f,6e,74,2d," &_
   "73,69,7a,65,3a,31,30,2e,30,70,74,3b,0d,0a,09,6d,73,6f,2d,62,69,64,69,2d,66," &_
   "6f,6e,74,2d,73,69,7a,65,3a,31,31,2e,30,70,74,3b,0d,0a,09,66,6f,6e,74,2d,66," &_
   "61,6d,69,6c,79,3a,22,56,65,72,64,61,6e,61,22,2c,22,73,61,6e,73,2d,73,65,72," &_
   "69,66,22,3b,0d,0a,09,6d,73,6f,2d,61,73,63,69,69,2d,66,6f,6e,74,2d,66,61,6d," &_
   "69,6c,79,3a,56,65,72,64,61,6e,61,3b,0d,0a,09,6d,73,6f,2d,68,61,6e,73,69,2d," &_
   "66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,56,65,72,64,61,6e,61,3b,0d,0a,09,6d,73," &_
   "6f,2d,62,69,64,69,2d,66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,22,54,69,6d,65,73," &_
   "20,4e,65,77,20,52,6f,6d,61,6e,22,3b,0d,0a,09,6d,73,6f,2d,62,69,64,69,2d,74," &_
   "68,65,6d,65,2d,66,6f,6e,74,3a,6d,69,6e,6f,72,2d,62,69,64,69,3b,0d,0a,09,63," &_
   "6f,6c,6f,72,3a,23,31,46,34,39,37,44,3b,0d,0a,09,6d,73,6f,2d,74,68,65,6d,65," &_
   "63,6f,6c,6f,72,3a,64,61,72,6b,32,3b,7d,0d,0a,2d,2d,3e,0d,0a,3c,2f,73,74,79," &_
   "6c,65,3e,0d,0a,3c,2f,68,65,61,64,3e,0d,0a,0d,0a,3c,2f,68,74,6d,6c,3e,0d,0a", ",")

  arrComposeFontSimpleHex = Split ("3c,00,00,00,1f,00,00,f8,00,00,00,40,c8,00,00,00,00,00," &_
   "00,00,00,00,00,ff,00,22,56,65,72,64,61,6e,61,00,00,00,00,00,00,00,00,00,00," &_
   "00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00", ",")

  arrReplyFontSimpleHex = Split ("3c,00,00,00,1f,00,00,f8,00,00,00,00,c8,00,00,00,00,00,00," &_
   "00,1f,49,7d,00,00,22,56,65,72,64,61,6e,61,00,00,00,00,00,00,00,00,00,00,00," &_
   "00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00", ",")

  arrComposeFontComplex = ArrayHexToDec(arrComposeFontComplexHex)
  arrReplyFontComplex = ArrayHexToDec(arrReplyFontComplexHex)
  arrComposeFontSimple = ArrayHexToDec(arrComposeFontSimpleHex)
  arrReplyFontSimple = ArrayHexToDec(arrReplyFontSimpleHex)

  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ComposeFontComplex", arrComposeFontComplex
  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ReplyFontComplex", arrReplyFontComplex
  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ComposeFontSimple", arrComposeFontSimple
  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ReplyFontSimple", arrReplyFontSimple
End Sub

Function ArrayHexToDec(arrHex)
  Dim i, arrDec
  ReDim arrDec(UBound(arrHex))
  For i = 0 to UBound(arrHex)
    If arrHex(i) = "00" Then
      arrDec(i) = 0
    Else
      arrDec(i) = CByte("&H" & arrHex(i))
    End If
  Next
  ArrayHexToDec = arrDec
End Function

Windows 7 login scripts and missing network drives

Most Windows sysadmins use a Group Policy object to launch their login script. You may have noticed that Windows 7 and Vista fail to connect network drives defined in the login script if the user is a local admin and UAC is enabled. The script completes successfully and no error condition is encountered, but no drive mappings. Run it again manually and everything’s fine.

This is in fact by design, and it’s caused by the way UAC works. When you’re a member of the Administrators group and you log in, your account is busted down to a non-privileged user by UAC. This running context is completely separate from the context you get when you right-click Command Prompt and launch as an administrator. As you’ll probably have noticed, network drives connected in one context are not visible in the other. The problem is that GPO-based login scripts are being executed in the Administrator context – not the ordinary user context.

So, how do we get it working? Microsoft offer an unsupported kludge in KB937624 – getting around the issue by weakening Windows security and forcing network connections to be common to both user contexts. They carefully designed this not to be the case, so modifying the behaviour does seem like a bad idea.

However, Microsoft’s preferred solution (example launchapp.wsf script in the appendix of that page) is to use the GPO-triggered script to set a Scheduled Task to run immediately in the other (non-admin) context, and run your login script from there – much better.
The reasons I’m writing all this up are that:

  • Microsoft’s example script has some illegal character/line-wrap in there – copying and pasting it won’t work!
  • This method doesn’t work with XP so some forking logic is needed if you have mixed clients.
  • They make no allowances for multiple admin/non-admin users sharing the same PC.
  • This appears to be Microsoft’s sole example document of how to program using the Task Scheduler 2.0 API, and it neglects to define several absolutely essential object properties if you want to do something more useful than simply open Notepad.

My particular problem was that I needed to launch a script with a space character in the path, e.g.:

cscript.exe //nologo "\\domain.com\netlogon\departmentX users\logon.vbs"

For me changing this path name was not an option as there were many other dependencies. I spent a long time wondering why the API was eating my quotes as I fed it the above string and I tried various ways to escape them. Eventually I launched the Scheduled Tasks MMC tool (click on the root of Task Scheduler Library to see the job). Looking in the Action properties I realised that there are separate fields for the starting directory and for the arguments. Manually editing the job to use these got it working:
Task Properties Dialog
Frustratingly, there don’t seem to be any examples on the Web showing you how to populate these fields programmatically. Guessing the Arguments property was straighforward but StartIn is not a valid propery name. I read on Wikipedia that Task Scheduler 2.0 uses XML to store its jobs so I exported the job and viewed it. Luckily they used consistent property names in the XML (Arguments and WorkingDirectory) and I was able to set them in VBScript (see highlighted lines below).

There was an additional complication though. Once a user has run the Scheduled Task, it’s left behind on the system. In my initial testing this wasn’t a problem because I was testing admin users, but I soon discovered that a non-privileged user cannot delete and recreate the task if one created by another user already exists. So we need only schedule the task if the current user is running in an elevated security context. By far the simplest method is to parse the output of the whoami /groups command, as explained in this post:
http://blogs.technet.com/b/jhoward/archive/2008/11/19/how-to-detect-uac-elevation-from-vbscript.aspx

UPDATE – added some logic to prevent the login script from launching for RemoteApp sessions to Terminal Servers.

'launchapp.vbs, modified from Microsoft's launchapp.wsf
'launches a process as interactive user, NOT as the elevated privilege user context

Option Explicit

Const TriggerTypeRegistration = 7
Const ActionTypeExecutable = 0
Const FlagTaskCreate = 2
Const LogonTypeInteractive = 3

Dim strWorkingDirectory, strHostname, strOSVer, colProcessList, strUser, strDomain
Dim objNetwork, objComputer, objShell, objExec, objWMI, objItem, strScriptName, strStdOut

strWorkingDirectory = "\\domain.com\netlogon\DepartmentX Users"

'launch this login script
strScriptName = "logon.vbs"

Set objNetwork = CreateObject("WScript.Network")
Set objShell = CreateObject("WScript.Shell")
Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") 
strHostname = objNetwork.ComputerName
Set objComputer = GetObject("WinNT://" & strHostname & ",computer")
strOSVer = objComputer.OperatingSystemVersion

If strOSVer >= "6.0" Then
  If IsElevated() Then
    'Machine has UAC and user is elevated so use LAUNCHAPP.WSF Task Scheduler method based
    'on appendix from http://technet.microsoft.com/en-us/library/cc766208(WS.10).aspx

    'Are we launched in a RemoteApp session on a Terminal Server? If so, quit.
    Set colProcessList = objWMI.ExecQuery("Select * from Win32_Process Where Name = 'rdpshell.exe'")
    For Each objItem In colProcessList
      objItem.GetOwner strUser, strDomain
      'If we're an admin we can see all users' processes so we need to check only our own
      If strUser = objNetwork.UserName Then
        WScript.Quit
      End If  
    Next

    LaunchApp
  Else
    'User is not elevated, so launch the script normally
    objShell.Run "cscript.exe //nologo " & Chr(34) & strWorkingDirectory & "\" & strScriptName & Chr(34), 1
  End If
Else
  'This is a Windows XP/2003 machine, so launch the script normally
  objShell.Run "cscript.exe //nologo " & Chr(34) & strWorkingDirectory & "\" & strScriptName & Chr(34), 1
End If

Set objNetwork = nothing
Set objComputer = nothing
Set objShell = nothing

Function IsElevated()
  IsElevated = False
  strStdOut = ""
  Set objExec = objShell.Exec ("whoami /groups")
  Do While (objExec.Status = 0)
    WScript.Sleep 100
    If Not objExec.StdOut.AtEndOfStream Then
      strStdOut = strStdOut & objExec.StdOut.ReadAll
    End If
  Loop
  If InStr(strStdOut,"S-1-16-12288") Then
    IsElevated = True
  End If
  Set objExec = nothing
End Function

Sub LaunchApp
  Dim objTaskService
  Dim strTaskName, rootFolder, taskDefinition, triggers, trigger, Action

  'Create the TaskService object
  Set objTaskService = CreateObject("Schedule.Service")
  Call objTaskService.Connect()
  strTaskName = "Launch App As Interactive User"

  'Get a folder to create a task definition in
  Set rootFolder = objTaskService.GetFolder("\")

  'Delete the task if already present
  On Error Resume Next
  Call rootFolder.DeleteTask(strTaskName, 0)
  Err.Clear

  'Create the new task
  Set taskDefinition = objTaskService.NewTask(0)

  'Create a registration trigger
  Set triggers = taskDefinition.Triggers
  Set trigger = triggers.Create(TriggerTypeRegistration)

  'Create the action for the task to execute
  Set Action = taskDefinition.Actions.Create(ActionTypeExecutable)
  Action.Path = "cscript.exe"
  Action.Arguments = "//nologo " & strScriptName
  Action.WorkingDirectory = strWorkingDirectory

  'Register (create) the task
  call rootFolder.RegisterTaskDefinition(strTaskName, taskDefinition, FlagTaskCreate,,, LogonTypeInteractive)

  Set objTaskService = nothing
End Sub