Finding and Fixing Unquoted Service Paths, Pt1

So in my last blog post I talked about the ongoing discussion of the unquoted service path in Windows, as well as how to validate the problem still exists. The discussion on TechNet continues, but it looks like we are getting some attention to the fact this is real, even if most of us can’t believe Microsoft has let it go this long.

When I showed how to find this problem before, I showed you in the registry editor. While that MIGHT work for one machine and someone with some time on their hands, we’re ADMINS! And THIS IS WINDOWS. I demand more automation. I have 4000 computers to fix!

ryan

Turns out, I had it all along.

I’ve posted updated copies of all 3 scripts with progress output and instructions for executing in parallel here. The explanation below is still valid, but I have adjusted the handling of some output to clean up the pipeline.

For those of you wondering… Ryan IS around. You guys remember Jitterbug? The phone for old people with big buttons? He’s busy working on a prototype smartphone along the same lines…

So I’ve decided to cut this up into 3 parts.
Step 1, Get all service paths into an object collection
Step 2, Identify which service paths are bad and propose new ones
Step 3, Fix them

Because PowerShell is object oriented and easy to write in, I’ve started writing this resolution there. I can produce scripts that put my data into objects, can accept pipelines from other cmdlets, and can pipe into other cmdlets. With objects I can sort and format and output with PowerShell’s native syntax. My endgoal is something along these lines:

$var = Get-ADComputer -filter * | Get-SVCPath
$var | Where {$_.status -like "Failed"} #Who didn't respond?
$var | Where {$_.status -like "Retrieved"} # Show me the keys collected

So without further ado…

Finding Service Paths

Because I’m writing a PowerShell script, there are a few things I’d like it to be able to do. I want it to assume this computer if I don’t specify arguments, I want it to accept a selected list of computers either as input on the shell, or from a text file, and I want it to accept pipes from other cmdlets that spit out objects. Fortunately this is pretty easy to do.

[cmdletbinding()]
	Param ( #Define a Mandatory name input
	[Parameter(
	 ValueFromPipeline=$true,
	 ValueFromPipelinebyPropertyName=$true, 
	 Position=0)]
	 [Alias('Computer', 'ComputerName', 'Server', '__ServerName')]
	[string[]]$name = $ENV:Computername
	) #End Param

I honestly don’t remember where I picked up on that list of aliases from. I know there are other names being used out there, but I picked this up a few months back and it has worked for me. I think it was probably Ed Wilson, the Microsoft Scripting Guy. Even if it wasn’t him, he’s an outstanding resource.

Because I have both pipeline and arrayed input, I need to Process (for each object) as well as ForEach (for each item). Simple enough.

Process
{ #Process Each object on Pipeline
	ForEach ($computer in $name)
	{ #ForEach for singular or arrayed input on the shell
	#Do Something
	} #End ForEach
} #End Process

So, I’ve got input and structure out of the way; I can get into the meat of my script. I need to comb through a hive of the registry to find all the services that have ImagePaths and pull those out. I know PowerShell has a registry provider, but I’m not certain how to get what I want here from it. In addition, everyone running Windows 7 and down has most of the good PowerShell capabilities disabled by default (script execution, remoting). So I’ve elected to use REG to get the info and PowerShell (only enabled locally) to parse the results. This is less stress on someone who needs to run it, plus it allows me to interact with machines that don’t have PowerShell. Even better, with remoting turned on I can deliver the script to the remote machine and it will execute with REG locally and pipe me back the object results, so its minimal loss anyway.

When I call REG’s query function, I can specify a base path, give it the value name I’m looking for, and tell it to recurse. This is pretty straight forward.

REG QUERY \\$NAME\HKLM\SYSTEM\CurrentControlSet\Services /v ImagePath /s

What I get back on the shell is a little sloppy though

[blank line]
[key path]
[value name] [data type] [data value]
...x however many keys are found
Plus an extra line at the end that gives us a summary:
x Keys Found

The good news is, we’re only looking for one value name, and its always the same type, so its easy to split. So we store the output of the REG Query and we can parse the results.

The only problem is error handling. I need to control for machines that are offline or I am denied access to. Depending on which environment you use (shell, ISE, PowerGUI, etc), you get differen error returns. I tried for about 2 hours to get Try/Catch to work, but PowerShell doesn’t always get the type back to test against, nor does it always see the exception at all. What I elected to do in the end is divert standard error into standard out and test for it. The good thing about REG is every time it can’t do what you ask, it throws the word “ERROR” or “Denied”

$result = REG QUERY "\\$computer\HKLM\SYSTEM\CurrentControlSet\Services" /v ImagePath /s 2>&1
#Error output from this command doesn't catch, so we need to test for it...
	if ($result[0] -like "*ERROR*" -or $result[0] -like "*Denied*")
		{ #Only evals true when return from reg is exception
		}	
	else
		{
		#Clean up the format of the results array
		$result = $result[0..($result.length -2)] #remove last (blank line and REG Summary)
		$result = $result | ? {$_ -ne ""} #Removes Blank Lines
		} #End Else

So at the end of this, $result is either an error object (at elem 0), or a formatted array that contains a continual stream of KEY NAME, PATH DATA, KEY NAME, PATH DATA…

If the result is an error, then we simply create an object for this machine that says we found an error.

if ($result[0] -like "*ERROR*" -or $result[0] -like "*Denied*")
		{ #Only evals true when return from reg is exception
		$obj = New-Object -TypeName PSObject
		$obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
		$obj | Add-Member -MemberType NoteProperty -Name Status -Value "REG Failed"
		$obj | Add-Member -MemberType NoteProperty -Name Key -Value "Unavailable"
		$obj | Add-Member -MemberType NoteProperty -Name ImagePath -Value "Unavailable"
		[array]$collection += $obj
		}

If the result is key names and path values, we can create an object for that, but we need an object for each key. Because we have a giant even-numbered array, we can simply loop through it and add each key object to the collection.

else
		{
		#Clean up the format of the results array
		$result = $result[0..($result.length -2)] #remove last (blank line and REG Summary)
		$result = $result | ? {$_ -ne ""} #Removes Blank Lines
		$count = 0
		While ($count -lt $result.length)
			{
 
			$obj = New-Object -TypeName PSObject
			$obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
			$obj | Add-Member -MemberType NoteProperty -Name Status -Value "Retrieved"
			$obj | Add-Member -MemberType NoteProperty -Name Key -Value $result[$count]
			$pathvalue = $($result[$count+1]).Split("", 11) #split ImagePath return
			$pathvalue = $pathvalue[10].Trim(" ") #Trim out white space, left with just value data
			$obj | Add-Member -MemberType NoteProperty -Name ImagePath -Value $pathvalue
 
			[array]$collection += $obj
			$count = $count + 2
			} #End While
		} #End Else

When we’ve finished this machine, we spit out the results for this machine and clear the collection, close the foreach loop and continue, or close the process loop and continue to the next object.

	Write-Output $collection
	$collection = $null #reset collection

In the end, we end up with a script that is flexible, and returns us objects for each service path on each machine either piped or listed into the script. This can be sorted, searched, or otherwise formatted with standard PowerShell syntax. The first one I’d suggest you try is | Format-List (Some of those paths are long!).

Show me all the keys for machines that didn’t fail… | Where {$_.status -notlike “Failed”}

Show me what failed… | Where {$_.status -like “Failed”}

Looking for a particular service name and how many machines it was found on? | Where {$_.key -like “servicename”}

How about just one machine from a big list of results? | Where {$_.name -like “computername”}

My next post will be a script that accepts the object input from this script and looks for bad service paths, as well as proposes corrections. See you next time!

Here’s the whole script we made today:

#GET-SVCpath.ps1
[cmdletbinding()]
	Param ( #Define a Mandatory name input
	[Parameter(
	 ValueFromPipeline=$true,
	 ValueFromPipelinebyPropertyName=$true, 
	 Position=0)]
	 [Alias('Computer', 'ComputerName', 'Server', '__ServerName')]
	[string[]]$name = $ENV:Computername
	) #End Param
 
Process
{ #Process Each object on Pipeline
	ForEach ($computer in $name)
	{ #ForEach for singular or arrayed input on the shell
	  #Try to get SVC Paths from $computer
	$result = REG QUERY "\\$computer\HKLM\SYSTEM\CurrentControlSet\Services" /v ImagePath /s 2>&1
	#Error output from this command doesn't catch, so we need to test for it...
	if ($result[0] -like "*ERROR*" -or $result[0] -like "*Denied*")
		{ #Only evals true when return from reg is exception
		$obj = New-Object -TypeName PSObject
		$obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
		$obj | Add-Member -MemberType NoteProperty -Name Status -Value "REG Failed"
		$obj | Add-Member -MemberType NoteProperty -Name Key -Value "Unavailable"
		$obj | Add-Member -MemberType NoteProperty -Name ImagePath -Value "Unavailable"
		[array]$collection += $obj
		}	
	else
		{
		#Clean up the format of the results array
		$result = $result[0..($result.length -2)] #remove last (blank line and REG Summary)
		$result = $result | ? {$_ -ne ""} #Removes Blank Lines
		$count = 0
		While ($count -lt $result.length)
			{
 
			$obj = New-Object -Typename PSObject
			$obj | Add-Member -Membertype NoteProperty -Name ComputerName -Value $computer
			$obj | Add-Member -MemberType NoteProperty -Name Status -Value "Retrieved"
			$obj | Add-Member -MemberType NoteProperty -Name Key -Value $result[$count]
			$pathvalue = $($result[$count+1]).Split("", 11) #split ImagePath return
			$pathvalue = $pathvalue[10].Trim(" ") #Trim out white space, left with just value data
			$obj | Add-Member -MemberType NoteProperty -Name ImagePath -Value $pathvalue
 
			[array]$collection += $obj
			$count = $count + 2
			} #End While
		} #End Else
	Write-Output $collection
	$collection = $null #reset collection
	} #End ForEach
} #End Process

Jeff is a system administrator for an enterprise of over 4000 devices. He is comfortable on Windows and *nix and participates in everything from desktop to network support. In addition, he develops in PowerShell to aid in the automation of administrative tasks.

When not at work, Jeff is adjunct faculty at the University of Alaska Anchorage, where he teaches PC Architecture to first year students in the Computer and Networking Technology Program. He is also working on new curriculum in PowerShell and Virtualization as well as leading department technology deployments.


View Jeff Liford's profile on LinkedIn



Posted in Computer Security, Desktop, PowerShell, Server