Speed up powershell script for Registry search (cu

2019-07-18 17:10发布

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

Update 1: Changed $d= Get-Item . to $d= Get-Item -literalpath $path. Need to use Set-Location also to avoid error with Get-ItemProperty, which occurs because the $path is not a valid object

How can I speed this code up? Whats wrong?

Please help to speed up...

#regsearch.ps1
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 then 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 Main Key
  Get-RegItems -path $path -match $match

  # Retrieve Items of all Childkeys
  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}  

#regsearch.ps1

5条回答
戒情不戒烟
2楼-- · 2019-07-18 17:45

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!

# carsten.giese@googlemail.com
# 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."
查看更多
一纸荒年 Trace。
3楼-- · 2019-07-18 17:53

Here is a faster version of you sample-script. Lasts ca. 1 minute on my machine. If you need it faster, then you need to work with advapi32.dll-Pinvokes, but then it will end quite complex.

Function Get-RegItems {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$path,
        [string]$match
    )
    #write-host $path.Substring(30)
    $key = Get-Item -literalpath $path
    ForEach ($entry in $key.Property) {
        $value = $key.GetValue($entry)
        if (($entry -match $match) -or ($value -match $match ) -or ($path -match $match)) { 
            write-host "key=$path property=$entry value=$value"
        }
    }
}

Function RegSearch {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$path,
        [string]$match
    )
    Get-RegItems -path $path -match $match
    ForEach ($item in get-ChildItem -literalpath $path -ea 0) {
        RegSearch -path $item.PsPath -match $match 
    }
}

cls
Remove-Variable * -ea 0
[System.GC]::Collect()

$searchkey =‘HKLM:\SOFTWARE\Microsoft\Office’ 
$pattern = "EventSystem"
measure-command {
    $result = RegSearch -path $searchkey -match $pattern
}
查看更多
太酷不给撩
4楼-- · 2019-07-18 17:57

Don't use the registry drive provider, if you want it faster.

查看更多
Root(大扎)
5楼-- · 2019-07-18 18:04

User function call overhead (scriptblocks included) is extremely big (e.g. 0.1-1ms). This becomes a very serious issue when the function is executed thousands/millions of times. Surprisingly, it's not mentioned in optimization-related articles (at least I've never seen it and I googled this topic a lot).

Unfortunately, the only only real solution to this particular issue is to inline the code at the cost of duplication and reduced readability.

Optimization should include code profiling.
PowerShell doesn't have a code profiler so you'll need to do it manually with Measure-Command.
Use System.Diagnostics.Stopwatch inside loops to display the accumulated time:

# global stopwatch
$sw1 = [Diagnostics.Stopwatch]::new()
$sw2 = [Diagnostics.Stopwatch]::new()
............
forEach(....) {
    ........
    $sw1.start()
    ........
    $sw1.stop()
    ........
    $sw2.start()
    ........
    $sw2.stop()
    ........
}
............
echo $sw1.ElapsedMilliseconds, $sw2.ElapsedMilliseconds
查看更多
放我归山
6楼-- · 2019-07-18 18:07

The single biggest improvement you can make here is changing:

Set-Location -literalpath $path 
$d= Get-Item .

to

$d = Get-Item -LiteralPath $path

Manipulating the location stack for each key in the hierarchy introduces A LOT of unnecessary overhead

查看更多
登录 后发表回答