Search code examples
performancepowershellforeachpowershell-2.0powershell-3.0

Speed up PowerShell script for Windows Registry search (currently 30 minutes)


I'm working on a script for use in Windows 7 and Windows 10 for a Windows Registry search in HKLM:\Software\Classes. So far my code works, but it's extremely slow. It takes about 30 minutes to complete.

I need to use Set-Location also to avoid an error with Get-ItemProperty, which occurs because the $path is not a valid object.

How can I speed this code up? What's wrong?

File regsearch.ps1 (Mathias R. Jessen's answer applied)

Function Get-RegItems
{
    Param(
        [Parameter(Mandatory=$true)]
        [string]$path,
        [string]$match)

    #Set Local Path and ignore wildcard (literalpath)
    Set-Location -literalpath $path
    $d = Get-Item -literalpath $path

    # If more than one value -> process
    If ($d.Valuecount -gt 0) {
        $d |
        # Get unkown property
        Select-Object -ExpandProperty Property |
            ForEach {
                $val = (Get-ItemProperty -Path . -Name $_).$_
                #if Filter $match found, generate ReturnObject
                if (($_ -match $match) -or ($val -match $match ) -or ($path-match $match)) {
                    New-Object psobject -Property @{ “key”=$path; “property”=$_; “value” = $val ;}
                }
            }
    }
} #end function Get-RegItems

Function RegSearch
{
    Param(
        [Parameter(Mandatory=$true)]
        [string]$path,
        [string]$match)

    # Expand $path if necessary to get a valid object
    if ($path.Indexof("HKEY") -ne "-1" -and $path.Indexof("Registry::") -eq "-1" )  {
        $path = "Microsoft.PowerShell.Core\Registry::" +$path
    }

    # Retrieve items of the main key
    Get-RegItems -path $path -match $match

    # Retrieve items of all child keys
    Get-ChildItem $path -Recurse -ErrorAction SilentlyContinue |
        ForEach {
            Get-RegItems -path $_.PsPath -match $match
        }
} #end function RegSearch


#$search = "HKCU:\SOFTWARE\Microsoft\Office"
$searchkey = ‘HKLM:\SOFTWARE\Microsoft\Office\’
#$searchkey = "HKLM:\Software\Classes\"
$pattern = "EventSystem"

cls
$result = @()

Measure-Command {$result = Regsearch -path $searchkey -match $pattern }

# TESTING
#$t = @( "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\Software\Classes",
#       "HKLM:\Software\Classes\Wow6432Node\CLSID\",
#       "HKCU:\SOFTWARE\Microsoft\Office\",
#       "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office")
#cls
#$t |ForEach { Get-RegItems -path $_ } | fl

if ($result.Count) {
    $result
    "Count: {0}" -f ($result.Count-1)
}
else {
    "Path: {0} `nNo Items found" -f $searchkey
}

Solution

  • I accepted the challenge and made it "as fast as possible". Now it is even faster than REGEDIT or any other tool. The below sample lasts 11 seconds to parse the complete OFFICE-key and all subkeys.

    In addition, it also searches for string-matches in REG-BINARY etc.

    Enjoy!

    # [email protected]
    # reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
    
    cls
    remove-variable * -ea 0
    $ErrorActionPreference = "stop"
    
    $signature = @'
    [DllImport("advapi32.dll")]
    public static extern Int32 RegOpenKeyEx(
        UInt32 hkey,
        StringBuilder lpSubKey,
        int ulOptions,
        int samDesired,
        out IntPtr phkResult
        );
    
    [DllImport("advapi32.dll")]
    public static extern Int32 RegQueryInfoKey(
        IntPtr hKey,
        StringBuilder lpClass, Int32 lpCls, Int32 spare, 
        out int subkeys, out int skLen, int mcLen, out int values,
        out int vNLen, out int mvLen, int secDesc,                
        out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
    );
    
    [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
    public static extern Int32 RegEnumValue(
      IntPtr hKey,
      int dwIndex,
      IntPtr lpValueName,
      ref IntPtr lpcchValueName,
      IntPtr lpReserved,
      out IntPtr lpType,
      IntPtr lpData,
      ref int lpcbData
    );
    
    [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
    public static extern Int32 RegEnumKeyEx(
      IntPtr hKey,
      int dwIndex,
      IntPtr lpName,
      ref int lpcName,
      IntPtr lpReserved,
      IntPtr lpClass,
      int lpcClass,
      out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
    );
    
    [DllImport("advapi32.dll")]
    public static extern Int32 RegCloseKey(IntPtr hkey);
    '@ 
    $reg = add-type $signature -Name reg -Using System.Text -PassThru
    $marshal = [System.Runtime.InteropServices.Marshal]
    
    function search-RegistryTree($path) {
    
        # open the key:
        [IntPtr]$hkey = 0
        $result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25,[ref]$hkey)
        if ($result -eq 0) {
    
            # get details of the key:
            $subKeyCount  = 0
            $maxSubKeyLen = 0
            $valueCount   = 0
            $maxNameLen   = 0
            $maxValueLen  = 0
            $time = $global:time
            $result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time)
            if ($result -eq 0) {
               $maxSubkeyLen += $maxSubkeyLen+1
               $maxNameLen   += $maxNameLen  +1
               $maxValueLen  += $maxValueLen +1
            }
    
            # enumerate the values:
            if ($valueCount -gt 0) {
                $type = [IntPtr]0
                $pName  = $marshal::AllocHGlobal($maxNameLen)
                $pValue = $marshal::AllocHGlobal($maxValueLen)
                foreach ($index in 0..($valueCount-1)) {
                    $nameLen  = $maxNameLen
                    $valueLen = $maxValueLen
                    $result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen)
                    if ($result -eq 0) {
                        $name = $marshal::PtrToStringUni($pName)
                        $value = switch ($type) {
                            1 {$marshal::PtrToStringUni($pValue)}
                            2 {$marshal::PtrToStringUni($pValue)}
                            3 {$b = [byte[]]::new($valueLen)
                               $marshal::Copy($pValue,$b,0,$valueLen)
                               if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) {
                                    [System.Text.Encoding]::Unicode.GetString($b)
                               } else {
                                    [System.Text.Encoding]::UTF8.GetString($b)}
                               }
                            4 {$marshal::ReadInt32($pValue)}
                            7 {$b = [byte[]]::new($valueLen)
                               $marshal::Copy($pValue,$b,0,$valueLen)
                               $msz = [System.Text.Encoding]::Unicode.GetString($b)
                               $msz.TrimEnd(0).split(0)}
                           11 {$marshal::ReadInt64($pValue)}
                        }
                        if ($name -match $global:search) {
                            write-host "$path\$name : $value"
                            $global:hits++
                        } elseif ($value -match $global:search) {
                            write-host "$path\$name : $value"
                            $global:hits++
                        }
                    }
                }
                $marshal::FreeHGlobal($pName)
                $marshal::FreeHGlobal($pValue)
            }
    
            # enumerate the subkeys:
            if ($subkeyCount -gt 0) {
                $subKeyList = @()
                $pName = $marshal::AllocHGlobal($maxSubkeyLen)
                $subkeyList = foreach ($index in 0..($subkeyCount-1)) {
                    $nameLen = $maxSubkeyLen
                    $result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time)
                    if ($result -eq 0) {
                        $marshal::PtrToStringUni($pName)
                    }
                }
                $marshal::FreeHGlobal($pName)
            }
    
            # close:
            $result = $reg::RegCloseKey($hkey)
    
            # get Tree-Size from each subkey:
            $subKeyValueCount = 0
            if ($subkeyCount -gt 0) {
                foreach ($subkey in $subkeyList) {
                    $subKeyValueCount += search-RegistryTree "$path\$subkey"
                }
            }
            return ($valueCount+$subKeyValueCount)
        }
    }
    
    $timer = [System.Diagnostics.Stopwatch]::new()
    $timer.Start()
    
    # setting global variables:
    $search = "enterprise"
    $hive   = [uint32]"0x80000002" #HKLM
    $subkey = "SOFTWARE\Microsoft\Office"
    $time   = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
    $hits   = 0
    
    write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
    $count = search-RegistryTree $subkey
    
    $timer.stop()
    $sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
    write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."