Tag Archives: Scripting

Manifests and Digital Signatures for Self Extracting Scripts

It has been quite in this little corner of the blogosphere lately. Must be because I am not doing anything, right?

Wrong. I have run out of time to blog. But today I will make an exception because I need to provide an update to an old post:
https://jgregscorral.wordpress.com/2012/09/13/wifi-profiles-for-windows-8//

As a substitute for becoming a real programmer, I have for years been writing VBScripts and wrapping them up with the Z-Zip Self-Extracting executable. After the release of Windows 8, this model became more difficult. Out-of-box, the 7-Zip self extractor started generating application compatibility troubleshooter pop-ups on clients. Even prior to that, clients would get warnings asking them “do you really want to execute this scary unsigned possibly-from-a-murdering-hacker” when they launched our executables.

The solution for this is, of course, to add an application manifest to the self-extractor, and then to digitally sign the resulting executable. Easy, right?

I actually did this a few years ago for our venerable Wi-Fi profile installation tool. It was not quite easy, and unfortunately I never did get the process fully automated. The roadblock was in automating the addition of a manifest to the application. Microsoft’s tool for this, “mt.exe”, from the Windows SDK, consistently corrupts my executables. Others in the blogosphere have identified the tool “Resource Hacker” to fill this need:
http://angusj.com/resourcehacker/
https://www.autoitscript.com/forum/topic/113657-autoit-and-manifest/

I added this tool to my ugly-old script packaging batch files, and had good success with eliminating the program compatibility dialogs:

..bin\resource_hackerResourceHacker.exe -addoverwrite %fname%.exe, %fname%.exe, %fname%.manifest, 24,1,

I also was able to streamline the signing process with the following batch code:

set fname=fixThunderbirdMailboxPath
set SDKPath="C:Program Files (x86)Windows Kits10binx86"
set TimeStampURL="http://timestamp.verisign.com/scripts/timstamp.dll"
set /P CertPath="Enter the full path to the PKCS12/PFX signing certificate:"
set /P CertPass="Enter the password for certificate file:"
%SDKPath%signtool.exe sign /f "%CertPath%" /p "%CertPass%" /t %TimeStampUrl% /v %fname%.exe
Advertisements

Ping plotting with PowerShell

Waaaay back we used to use a spiffy little tool called “ping plotter” to discover vacant IP addresses on our subnets. I had not had to do an exhaustive study of this for awhile. When it came up again today, I thought “I’ll bet we can do that with two lines of PowerShell.” But I was wrong… it took three lines, since I needed to initiate an array variable:

#Initialize $range as an array variable:
$range = @()

#Populate $range with integers from 2 though 254 
#  (for a Class C ipv4 subnet):
for ($i=2; $i -le 254; $i++) {$range += $i}

#Write out IP addresses for systems that do not have registered DNS 
#  names in the IP subnet 192.168.1.1/24:
$range | % {$ip = "192.168.1." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {write-host $ip}}

Now some variations… write out only addresses with no DNS entry and that do not respond to ping. (This will help to weed out addresses that are in use that for whatever reason to not have a DNS name.):

$range | % {$ip = "132.198.102." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {$out2 = & ping $ip -n 1; `
  if ($out2 -match 'Destination host unreachable') {write-host $ip}}}

… and perhaps most usefully, write out addresses with no DNS entry and that cannot be located using ARP. (This will weed out in use addresses with no DNS and a firewall that blocks ICMP packets.):

$range | % {$ip = "132.198.102." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {& ping $ip -n 1 > $null; `
  $out2 = arp -a $ip; if ($out2 -match 'No ARP Entries') {write-host $ip}}}

Replacing DirSync with Microsoft Azure Active Directory Sync Services

Back in October, MS announced that the new “MS Azure AD Sync Services” (AAD Sync) had gone GA, which means that DirSync is now officially deprecated.

Ironically, I got wind that “Azure AD Connect” would be replacing Azure AD Sync Services, not but a few weeks before:

http://blogs.technet.com/b/ad/archive/2014/08/04/connecting-ad-and-azure-ad-only-4-clicks-with-azure-ad-connect.aspx

Deprecated on release?  Not quite… it appears that Azure AD Sync Services will be at the core of Azure AD Connect, so time spent migrating to Azure AD Sync Services will not be a complete waste of time.

I decided to make this migration happen now, because we will be ramping up to provision faculty and staff with access to Office 365 Pro Plus software in the near future, and I would like to be working with a tool that does not require me to run it in an unsupported configuration (we removed the “Replicating Directory Changes” right from our DirSync service account, which at least at one time was considered a Bad Thing To Do.).

The Good News:

  • The out-of-box configuration does not require a service account with the “Replicating Directory Changes” right, making filtering of sensitive user attributes much easier.
  • The initial setup wizard allows you to de-select attributes that you don’t want to sync to Azure.  No more wading though dense miisclient.exe dialogs and walking off-support to do basic filtering!  The require attributes are listed in grey, so that you don’t risk filtering an attribute that Azure absolutely must have.
  • AAD Sync actually has some documentation available on its installation and use over at MSDN:
    http://msdn.microsoft.com/en-us/library/azure/dn790204.aspx
    …And there actually are quite a few important details in this seemingly cursory documentation.
  • AAD Sync entirely replaces the UI-heavy, un-maintainable attribute filtering tools in DirSync with a new “Declarative Provisioning” engine, that allow robust attribute filtering and transformation rules to be defined by the sys admin.
  • Custom “Inbound” attribute filters will be preserved during product upgrades, according to the documentation.
  • The documentation has been updated with instructions on how to make domain-based sync filters actually work in a multi-domain forest.  This was a key detail that was missing in the DirSync documentation.

The Bad News:

  • The Declarative Provisioning language, while based on VB for Applications (The “best known language by systems administrators”, according to the docs.  Whaaaat?!?!), is not actually VBA.  Debugging methods for dealing with this new VB-like thing are not at all documented, and examples of its usage are few and far between.

So what did I learn during this deployment process that was Blog-worthy?  How to debug Declarative Provisioning!

Debugging Declarative Provisioning (Import Expression Filters):

Let’s take an example.  We want to sync only students, and users who are to be provisioned for use of Lync in Office 365 into Azure.  To accomplish this, we have populated students with the text value “Student” in extensionAttribute1.  Lync users have had extensionAttribute2 populated with the text value “lync”.

When running the Synchronization Rule Editor, we create a new “inbound” rule, assign it to object type “inetOrgPerson”, link type is “join”, and give it a precedence under 100.  We skip the Scoping and Join Rules options, and go to “Transformations”.

The flowType gets set to “Expression”, the Target Attribute to “cloudFiltered”, and we define a VBA-like expression in the “Source” field:

EditSyncRule

My expression follows:

IIF(CBool(InStr([extensionAttribute1], "Student", 1)), NULL, IIF(CBool(InStr([extensionAttribute2], "lync", 1)), NULL, True)

So what is the intent here? “cloudFiltered” is a metaverse attribute that can be set to suppress synchronization of an account upon export to Azure AD.  If set to “True”, the account should not get synced to Azure.
(Note: The expression that we use was updated on 2014-12-22 as follows:

 
IIF(CBool(InStr([extensionAttribute1], "Student", 1)), NULL, IIF(CBool(InStr([extensionAttribute1], "Faculty", 1)), NULL, IIF(CBool(InStr([extensionAttribute1], "Staff", 1)), NULL, True)))

This change was necessary to allow provisioning of faculty and staff accounts with the new Office 365 ProPlus Faculty/Staff benefit.)

Our expression uses the “IIF” fuction to check to see if “extensionAttrubute1” contains “Student”.  If so, then “IIF” returns “NULL”, and NULL is fed to the “cloudFiltered” attribute (which, according to the docs, will cause the attribute to be cleared if it is already set).  However, if extensionAttribute1 does not contain “Student”, we perform a second test to see if “extensionAttribute2” contains “lync”.  If so, cloudFiltered again gets set to NULL.  If “extensionAttribute2” does not contain “lync”, then “cloudFiltered” gets set to the boolean value “True”.

Syntax quirks:

  • Items enclosed in square braces [] refer to metaverse attributes, and serve as a sort of automatic variable for provisioning.
  • Note that the InStr() function used here is not the same one documented in the VB for Applications language reference:
    http://msdn.microsoft.com/en-us/library/office/gg264811(v=office.15).aspx
    The “start position” parameter in the docs is not supported here, although it does appear that the “compare” parameter is supported (represented by the “1” in my example, which means perform a case-insensitive comparison).
  • Everything is case sensitive… even the function names!  “IIF()” is all caps.  CBool(), CStr(), InStr(), and Error() are all “Caml Cased”.
  • IIF() is a bit of a strange beast by itself.  You might need this syntax reference:
    http://msdn.microsoft.com/en-us/library/office/gg264412(v=office.15).aspx

Now that we have a filter, how to we test it?  The docs say “Run a full import followed by a full sync” on the AD management agent.  While this will work, it also is a bit time consuming.  If there are syntax errors in your expression, you will get a lot of errors in your sync logs.  Is there something better?

As it turns out, yes there is.  Special thanks to Brian Desmond (co-author of the o’Riley press “Active Directory Cookbook”) for putting me on the right track:

  1. In the MIISClient.exe, right click your AD management agent, and select “Search Connector Space”:
    SearchConnectorSpace
  2. Specify the DN of a user that you want to test your expression on in the search box.  You can quickly get a DN using PowerShell with the ActiveDirectory module:
    Get-ADUser -Identity [samAccountName]
    Click “Search”, click the returned object, and click “Preview”:
    PreviewUser
  3. In the “Preview” dialog, select either a Full or Delta sync, then click “Generate Preview”, and select the “Import Attribute Flow” option on the left.  Review the list of Sync Rules on the bottom pane to make sure your rule was processed.  Any rule that generates a change will be displayed in the top pane:
    ImportFlow
    In this case, the expression did not generate a change in the existing metaverse object.
  4. If an expression that you have authored generates an error, a full stack trace will appear in the Error Details section of the Preview pane.  Scroll down to the bottom of the stack, where it is likely that the actual problem with your expression will be identified:
    PreviewError
    In this example, I inserted an “Error()” function with a null argument.  The language engine did not like this.
  5. Back in the “Start Preview” tab, you could “Commit Preview” to apply the filter rules for this specific object, and then view the results in the Metaverse Search component of miisclient.exe.

Using the preview tool, I was able to get my filter expressions working correctly with minimal fuss and delay.  The final hurdle to getting filtered attributes set correctly was understanding what the various “run profiles” in AAD Sync actually do.

  • AD Agent – Full Import:  Appears to sync data from AD to the AAD Sync Metaverse without applying Declarative Provisioning Filters.  I think.  Not 100% on this one.
  • AD Agent – Full Synchronization:  Appears to apply Declarative Provisioning filters on all metaverse objects.
  • Azure AD Agent – Full Sync: Appears to apply export filtering logic to metaverse objects entering the Azure connector space.
  • Azure AD Agent – Export:  Appears to sync metaverse objects to Azure without applying filtering logic.  My impression is that you must do a “sync” before an export, if you want logic applied to Azure AD objects as you intended.

My understanding of the run profiles may be flawed, but I will note that if I perform syncs in the following sequence, I get the expected results:

AD Agent:Full Import -> AD Agent:Full Sync -> Azure Agent:Full Sync -> Azure Agent:Export

I am not sure what the “authoritative” way to perform synchronizations might be, but this works for me. When trying other sequences, I have seen strange results such as metaverse objects not getting updated with the intended filter results, or objects getting declared as “disconnectors” by the Azure AD Agent, and thus getting deleted from Azure.  Ugh!

That’s all for today… hopefully this info will help keep someone else out of trouble, too.

Bulk-modification of deployment deadlines in SCCM

A full two years into testing we finally are moving forward with production deployment of our Configuration Manager 2012 (SCCM) environment. Last month we (recklessly?) migrated 1000 workstations into the environment. While the deployment was a technological success, it was a bit of black-eye in the PR department.

Client computers almost uniformly did an unplanned reboot one hour after the SCCM agent got installed on their workstations. In addition to that, many clients experienced multiple reboot requests over the coming days. Many client reported that they did not get the planned 90-minute impending reboot warning, but only the 15-minute countdown timer.

Lots of changes were required to address this situation:

See the “Suppress and required client restarts” setting documented here:
http://technet.microsoft.com/en-us/library/gg682067.aspx#BKMK_EndpointProtectionDeviceSettings
This one was causing clients to reboot following upgrade of their existing Forefront Endpoint Protection client to SCEP. That explained the unexpected 60-minute post-install reboot.

Next, we decided to change the reboot-after deadline grace period from 90 minutes to 9 hours, with the final warning now set to one hour, up from 15 minutes. This should allow people to complete work tasks without having updates interrupt their work day.

Finally, we are planning to reset the deployment deadline for all existing software update deployments to a time several days out from the initial client installation time. Since we have several dozen existing software update group deployments, we need a programmatic approach to completing this task. The key to this was found here:
http://www.scconfigmgr.com/2013/12/01/modify-the-deadline-time-of-an-adr-deployment-with-powershell/
Thanks to Nickolaj Andersen for posting this valuable script.

It did take me a bit of time to decode what Nickolaj was doing with his script (I was not already familiar with the Date/Time format generally used in WMI). I modified the code to set existing update group deployments to a fixed date and time provided by input parameters. I also added some in-line documentation to the script, and added a few more input validation checks:

We additionally could push out the deployment time for Application updates as well using the “SMS_ApplicationAssignment” WMI class:
http://msdn.microsoft.com/en-us/library/hh949469.aspx

In this case, we would want to change the “UpdateDeadline” property, since we do not set a “deployment deadline” for these updates, but instead are using application supersedence rules to control when the updates are deployed.

Using SQL Logins with “AlwaysOn” Availability Groups

I had some fun today configuring a SQL 2012 “AlwaysOn” Availability Group working with a database that was configured to use SQL Logins.  I am working with the vendor to see if we can use Integrated Authentication instead, but in the meantime I managed to get failover functional.

The problem I was experiencing was that on failover, the database users defined in the database lost their mappings to the SQL Login on the server.  I had created SQL Logins on both nodes of the Availability Group with identical usernames and passwords, but the mapping still failed.  Turns out this was happening because SQL Logins (like AD and NT accounts) have SIDs, and these SIDs are used to map database users to logins.  To correct the problem, I needed to create SQL Logins with identical SIDs on the two servers.

Procedure:

  1. Create SQL Logins on one node of the Availability Group and perform mappings.
  2. Lookup the SIDs of the users using the following query:
    SELECT SUSER_SID (‘[SqlLogin]’)
  3. Script out creation of the matching logins:
    USE [master]
    GO
    CREATE LOGIN [SqlLogin] WITH PASSWORD=N'[Password]', SID=[HexidecimalSID], DEFAULT_DATABASE=[myDatabase], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
    GO
    
  4. Failover the Availability Group and verify that user mappings are working as planned.  Verify that passwords are working, too.

Another useful tidbit here is a script to re-map SQL database users to local SQL logins:

USE myDatabase
GO
sp_change_users_login @Action='update_one', @UserNamePattern='databaseUserName', @LoginName='SqlLoginName';
GO

Really I am surprised that I did not run into this problem with our SQL 2008 mirrored databases.  The problem should have been present there as well.

Scriptomatic Access to the Start Menu and Taskbar

As promised in my previous post, here is my current VBScript for configuring the Windows 7 Start Menu and Taskbar. Not beautiful, but certainly functional. My thanks to JuliusPIV and cogumel0 for doing the heavy lifting that made this script possible.

Note that you really will need to set the Group Policy option to turn off the Start Menu program history if you want Start Menu pinning to be at all effective in streamlining the Windows 7 “first time” GUI.

'==========================================================================
'
' NAME: Pin & Unpin items to/from Start Menu & Taskbar
'
' AUTHOR: J. Greg Mackinnon
' DATE  : 2013-01-31
'
' COMMENT: Derived from code by JuliusPIV found here:
'   http://social.technet.microsoft.com/Forums/en/w7itproinstall/thread/73eb1c0a-fc78-4ae7-ba6d-356d9a9a5328
'
'   To add items to Start Menu or Taskbar, add a variable defining the 
'   path to the original link in the variables section, then add that 
'   variable to the "aPinSM", "aPinTB", or "aUnpinTB" arrays.
'
'   Note that not all links (such as filesystem shortcuts) can be pinned.
'
'   Uncomment "debugecho" lines to troubleshoot.
'
'==========================================================================
option explicit

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        CONSTANTS
'=-=-=-=-=-=-=-=-=-=-=-=-=
'List of "Shell Special Folder Constants" used in the script.  See:
'http://msdn.microsoft.com/en-us/library/windows/desktop/bb774096(v=vs.85).aspx
const ssfAPPDATA = &H1a
const ssfCOMMONPROGRAMS = &H17
const ssfPROGRAMFILESx86 = &H30
const ssfPROGRAMS = &H2 
const ssfSYSTEM = &H25
const ssfWINDOWS = &H24

'=-=-=-=-=-=-=-=-=-=-=-=-=
'         OBJECTS
'=-=-=-=-=-=-=-=-=-=-=-=-=
dim fso, oShell, oShortcut
set fso = CreateObject("Scripting.FileSystemObject")
set oShell = CreateObject("Shell.Application") 

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        VARIABLES
'=-=-=-=-=-=-=-=-=-=-=-=-=
dim aPinSM, aPinTB, aUnpinTB
dim bEchoOut, bPinItem
dim sAUP, sUP, sRAD, sPFx86, sSys32, sItem, sScriptHost, sFileName
dim sGC, sMOW, sMOE, sMOPP, sMOON, sMOO, sOC, sFZ, sPT, sProj, sCalc, sSnip, sPDN, sMag, sKey, sWMP

'Configure variables for well known folders:
sRAD = oShell.NameSpace(ssfAPPDATA).Self.Path & ""            'Roaming AppData
sAUP = oShell.NameSpace(ssfCOMMONPROGRAMS).Self.Path & ""     'Start Menu Programs - All Users
sUP = oShell.NameSpace(ssfPROGRAMS).Self.Path & ""            'Start Menu Programs - Current User
'sPFx86 = oShell.NameSpace(ssfPROGRAMFILESx86).Self.Path & "" 'Program Files (x86)
'sSys32 = oShell.NameSpace(ssfSYSTEM).Self.Path & ""          '%WinDir%system32

'List of links to be added to the Start Menu or Taskbar, relative to:
' C:ProgramDataMicrosoftWindowsStart MenuPrograms
sGC = sAUP & "Google ChromeGoogle Chrome.lnk"
sMOW = sAUP & "Microsoft Office 2013Word 2013.lnk"
sMOE = sAUP & "Microsoft Office 2013Excel 2013.lnk"
sMOPP = sAUP & "Microsoft Office 2013PowerPoint 2013.lnk"
sMOON = sAUP & "Microsoft Office 2013Onenote 2013.lnk"
sMOO = sAUP & "Microsoft Office 2013Outlook 2013.lnk"
sOC = sAUP & "Oracle CalendarOracle Calendar.lnk"
sFZ = sAUP & "FileZilla FTP ClientFileZilla.lnk"
sPT = sAUP & "PuTTYPuTTY.lnk"
sProj = sAUP & "Accessoriesdisplayswitch.lnk"
sCalc = sAUP & "AccessoriesCalculator.lnk"
sSnip = sAUP & "AccessoriesSnipping Tool.lnk"
sPDN = sAUP & "Paint.NET.lnk"
sMag = sUP & "AccessoriesAccessibilityMagnify.lnk"
sKey = sUP & "AccessoriesAccessibilityOn-Screen Keyboard.lnk"
sWMP = sRAD & "MicrosoftInternet ExplorerQuick LaunchUser PinnedTaskBarWindows Media Player.lnk"

'Arrays containing links to be added to StartMenu or Taskbar, or to be removed from the Taskbar:
aPinSM = Array(sOC,sFZ,sPT,sPDN,sSnip,sCalc,sProj,sMag,sKey)
aPinTB = Array(sGC,sMOW,sMOE,sMOPP,sMOON)
aUnpinTB = Array(sWMP,sMOW,sMOPP)

'=-=-=-=-=-=-=-=-=-=-=-=-=
'   FUNCTIONS AND SUBS
'=-=-=-=-=-=-=-=-=-=-=-=-=
function PinSM(shortcut)
	dim oFolder, oFolderItem
	dim sFolder, sFile
	dim colVerbs
	dim itemverb
	
	sFolder = fso.GetParentFolderName(shortcut)
	sFile = fso.GetFileName(shortcut)

	'debugecho "Pinning " & sFolder & "" & sFile & " to Start Menu."
	Err.Clear
					
	set oFolder = oShell.NameSpace(sFolder)
	set oFolderItem = oFolder.ParseName(sFile)
	set colVerbs = oFolderItem.Verbs

	for each itemverb in oFolderItem.Verbs
		if Replace(itemverb.name, "&", "") = "Pin to Start Menu" then itemverb.DoIt
	next
end function

function PinTB(shortcut)
	dim sFolder, sFile
	dim oFolder, oFolderItem
	dim colVerbs, itemverb
	
	sFolder = fso.GetParentFolderName(shortcut)
	sFile = fso.GetFileName(shortcut)

	'debugecho "Pinning " & sFolder & "" & sFile & " to Taskbar."
	Err.Clear
					
	set oFolder = oShell.NameSpace(sFolder)
	set oFolderItem = oFolder.ParseName(sFile)
	set colVerbs = oFolderItem.Verbs
	
	for each itemverb in oFolderItem.Verbs
		if Replace(itemverb.name, "&", "") = "Pin to Taskbar" then itemverb.DoIt
	next
end function

function UnpinTB(shortcut)
	dim sFolder, sFile
	dim oFolder, oFolderItem
	dim colVerbs, itemverb
	
	sFolder = fso.GetParentFolderName(shortcut)
	sFile = fso.GetFileName(shortcut)

	'debugecho "Unpinning " & sFolder & "" & sFile & " from Taskbar."
	Err.Clear
					
	set oFolder = oShell.NameSpace(sFolder)
	set oFolderItem = oFolder.ParseName(sFile)
	set colVerbs = oFolderItem.Verbs
	
	for each itemverb in oFolderItem.Verbs
		if Replace(itemverb.name, "&", "") = "Unpin from Taskbar" then itemverb.DoIt
	next
end function

function debugecho(msg)
	if bEchoOut then
		wscript.echo msg
	end if
end function

sub Main
	for each sItem in aUnpinTB
		if not fso.FileExists(sItem) then
			bPinItem = false
			'debugecho "File, " & sItem & ", to unpin does not exist."
			'debugecho "Please check the input and try again."
		else
			UnpinTB(sItem)
		end if
	next
	for each sItem in aPinSM
		if not fso.FileExists(sItem) then
			bPinItem = false
			'debugecho "File, " & sItem & ", to pin does not exist."
			'debugecho "Please check the input and try again."
		else
			PinSM(sItem)
		end if
	next
	for each sItem in aPinTB
		if not fso.FileExists(sItem) then
			bPinItem = false
			'debugecho "File, " & sItem & ", to pin does not exist."
			'debugecho "Please check the input and try again."
		else
			PinTB(sItem)
		end if
	next
end sub

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        MAIN BODY
'=-=-=-=-=-=-=-=-=-=-=-=-=
'Suppress echo if we are in WScript:
sScriptHost = LCase(Wscript.FullName)
if Right(sScriptHost, 11) = "wscript.exe" then
    bEchoOut = false
else
    bEchoOut = true
end if

call Main

Unattended Install and Upgrade of Adobe Reader

Previously we explored how to increase the success rate of unattended application upgrades using our handy “killAndExec” VBScript. This works well for about 80% of our applications. What about Adobe Reader? Well… not so much.

Thankfully, the stock Adobe Reader installer deals with open files quite nicely, and does not care if Reader is in use by a browser during silent installs (this is the main reason that we needed killAndExec.vbs in the first place). However, not all Reader install operations are full installs. Interestingly, Adobe is one of the few vendors that I deal with that actually uses MSI patch files (MSP). To install a patch release of Reader (i.e. 10.1.4), you need first to install the base version (10.1), then one or more patch MSP files. This is easy for new installs… just run “setup.exe” silently, then run “msiexec /p (patchFileName) /qn” to install the patch.

However, the situation gets more complicated for upgrades. If the base product already is installed, setup.exe will return an error code. So, for upgrade scenarios, I have put together another handy VBScript to handle base version detection. The script follows:

option explicit
' Install Adobe Reader Script:
' J. Greg Mackinnon, 2012-06-12
' Intended to perform unattended installations of Adobe Reader by MS SCCM 2012.
' 
' Installs the version of Adobe Reader in the same directory as the script, if not already installed.
' Also installs the current Reader patch, if requested.
' Installer string is specified in "sInstall".
' Requires: 
'     Adobe Reader setup and patch files in the same directory as the script.
'     "setup" and "basever" arguments required.
'     "patch" argument optional.
' Returns:
'     - Code 100 - if required arguments are not provided to the script.
'     - Return code of setup program added to the return code of the patch program, if no other errors occur.

dim oExec, oFS, oLog, oShell
dim cScrArgs
dim iExit
dim sBaseVer, sInstall, sLog, sOut, sPatch, sPath, sPF, sScrArg, sTemp, sVer
dim bDoPatch

sLog = "installAdobeReader.log"
iExit = cLng(0)

' Instantiate objects:
Set oShell = CreateObject( "WScript.Shell" )
Set oFS = CreateObject("Scripting.FileSystemObject")
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
Set oLog = oFS.OpenTextFile(sTemp & "" & sLog, 2, True)

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Define Functions
'
function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

sub subHelp
	echoAndLog "installAdobeReader.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Runs Adobe Reader silent setup (if not already present), then applies "
	echoAndLog "any specified MSP patch files for Reader."
	echoAndLog "Logs output to 'installAdobeReader.log' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/setup:""[setupFile]"""
	echoAndLog "     The primary Adobe Reader installation program.  If switches "
	echoAndLog "    are required for setup to run silently, they must be provided."
	echoAndLog "/basever:"
	echoAndLog "     The base Adobe Reader product version for which to check (i.e. 10.1)"
	echoAndLog ""
	echoAndLog "Optional arguments and syntax:"
	echoAndLog "/patch:""[patchFile]"""
	echoAndLog "     MSP patch file to install after Adobe Reader setup completes."
end sub
' End Functions
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Parse Arguments
'
if WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
'	For Each sScrArg in cScrArgs
'		echoAndLog sScrArg 'Echo supplied arguments to console
'	Next

	for each sScrArg in cScrArgs
		select case LCase(sScrArg)
			Case "setup"
				sInstall = cScrArgs.Item(sScrArg)
			Case "patch"
				sPatch = cScrArgs.Item(sScrArg)
			Case "basever"
				sBaseVer = CStr(cScrArgs.Item(sScrArg))
			Case Else
				echoAndLog vbCrLf & "Unknown switch or argument: " & sScrArg & "."
				echoAndLog "**********************************" & vbCrLf
				subHelp
				oLog.Close
				WScript.Quit(100)
		end select
	next
	if (IsNull(sInstall) or IsEmpty(sInstall)) then
		echoAndLog "Required argument 'setup' was not provided."
		echoAndLog "**********************************" & vbCrLf
		subHelp
		oLog.Close
		wscript.quit(100)
	elseif (IsNull(sBaseVer) or IsEmpty(sBaseVer)) then
		echoAndLog "Required argument 'basever' was not provided."
		echoAndLog "**********************************" & vbCrLf
		subHelp
		oLog.Close
		wscript.quit(100)
	elseif (IsNull(sPatch) or IsEmpty(sPatch)) then 
		bDoPatch = False
	else
		bDoPatch = True
	end if
elseif WScript.Arguments.Named.Count = 0 then 'Detect if required args are not defined.
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	oLog.Close
	WScript.Quit(100)
end if
'
' End Argument Parsing
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Begin Main
'

' Complete version and installation strings:
sInstall = "." & sInstall
sPatch = "msiexec.exe /p " & sPatch & " /qb /norestart"

' Build path to Adobe Reader executable:
sPF = oShell.ExpandEnvironmentStrings( "%ProgramFiles%" )
sPath = sPF & "AdobeReader 10.0ReaderAcroRd32.exe"
echoAndLog "Acrobat Reader Path: " & sPath

' Get the version string on the currently installed Reader executable:
on error resume next
sVer = oFS.GetFileVersion(sPath)
on error goto 0
echoAndLog "Version of currently installer Adober Reader: " & sVer

' See if we already have the base version installed:
if InStr(Left(cStr(sVer),4),sBaseVer) then
	'Reader 10.1.x is already installed skip base product installation.
	echoAndLog "Base product installed.  Skipping setup..."
else
	'Install the base product.
	echoAndLog "Installing base product with command: " & sInstall
	set oExec = oShell.Exec(sInstall)
	Do While Not oExec.Status  1
		WScript.Sleep(100)
	Loop 
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode)
end if

'Now install the patch:
if bDoPatch = True then
	echoAndLog "Patch installation requested."
	echoAndLog "Installing patch with command: " & sPatch
	set oExec = oShell.Exec(sPatch)
	Do While Not oExec.Status  1
		WScript.Sleep(100)
	Loop 
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from patch installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode) + iExit
else 
	echoAndLog "Patch installation was not requested.  Exiting."
end if

oLog.Close
wscript.quit(iExit)
'
' End Main
'''''''''''''''''''''''''''''''''''''''''''''''''''