ProcMon – Trace Process Activity

Traces the activity of the selected process ID
Version 2.3.5
Created on 2018-09-06
Modified on 2025-02-23
Created by Trentent Tye
Downloads: 19

The Script Copy Script Copied to clipboard
<#
  .SYNOPSIS
        Capture process information using process monitor.
  
    .DESCRIPTION
        This script borrows heavily from the great work done by: 
        Nick Atkins - @Nik_41tkins - http://nomanualrequired.blogspot.com/

        The license he included with his original script:

            This program is free software: you can redistribute it and/or modify
            it under the terms of the GNU General Public License as published by
            the Free Software Foundation, either version 3 of the License, or
            (at your option) any later version.

            This program is distributed in the hope that it will be useful,
            but WITHOUT ANY WARRANTY; without even the implied warranty of
            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
            GNU General Public License for more details.

            You should have received a copy of the GNU General Public License
            along with this program.  If not, see <http://www.gnu.org/licenses/>.

        This script will create a dynamic process monitor (procmon) filter using the process
        ID and start a capture lasting a duration you specify.
  
 .PARAMATER <PID <string[]>
  The process ID to set in the filter for the process monitor capture.
  
 .PARAMETER <Duration <string[]>
  The duration (in seconds) to run the capture.

 .PARAMETER  <ProcMonFolder <string[]>
  Specifies folder that contains procmon.exe.

    .PARAMETER  <SaveLogTo <string[]>
  Specifies folder the process monitor log file will be saved.
  
    .LINK
        For more information refer to:
            http://theorypc.ca

    .LINK
        Stay in touch:
        http://twitter.com/trententtye

    .EXAMPLE
        C:\PS> Remote-Procmon -ProcessId 3536 -Duration 15 -KeepAll -ProcMonFolder C:\sysinternals -SaveLogTo D:\procmonlogs
  
  Starts process monitor for 15 seconds, filtering for session 3, keeping all events except exclusions.  Procmon.exe is located in the path
        C:\sysinternals\procmon.exe and the logs will be saved to the D:\procmonlogs folder.

        C:\PS> . .\TraceProcessActivity.ps1 3536 15 C:\sysinternals D:\procmonlogs
#>

function Remote-Procmon {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]$ProcessID,

        [Parameter(Mandatory=$false)]
        [int]$Duration = 10,

        [Parameter(Mandatory=$false)]
        [switch]$KeepAll,

        [Parameter(Mandatory=$true)]
        [String]$ProcMonFolder,

        [Parameter(Mandatory=$true)]
        [String]$SaveLogTo
    )

    begin {
        function Convert-FilterToObj {
            [CmdletBinding()]Param(
                [ValidateNotNullOrEmpty()][String]$Filters
            )

            #Include profiling events only if paramater switch set
            if((-not($KeepAll)) -and (($Filters.length) -ne 0))
            {
                $Filters += ";EventClass,is,profiling,exclude"
            }

            $FilterObj = @()  #Init blank array
            #Filters must be seperated by ;
            $Filter = $filters.split(';')
            $Filter | ForEach-Object {
                $filterOptions =@()
                #Use builtin split method to convert comma seperated string into array
                foreach ($option in $_.split(',')) {
                    $filterOptions += $option
                }
                #Checking our array for exactly four objects
                if ($filterOptions.count -ne 4) {
                    Write-Error "Error: Filter `"$filterOptions`" is not in correct format."
                } else {
                    Write-Verbose "Filter `"$filterOptions`"."
                    #Convert filter into an object, remove spaces from Column/Relation
                    $CurrentFilter = New-Object system.Object
                    $CurrentFilter | Add-Member noteproperty -Name "Column" -Value $filterOptions[0].replace(' ','')
                    $CurrentFilter | Add-Member noteproperty -Name "Relation" -Value $filterOptions[1].replace(' ','')
                    $CurrentFilter | Add-Member noteproperty -Name "Value" -Value $filterOptions[2]
                    $CurrentFilter | Add-Member noteproperty -Name "Action" -Value $filterOptions[3]
                    #Add current filter to output object
                    $FilterObj += $CurrentFilter
                }
            }
            Write-Output $FilterObj
        }

     function Convert-LargeValues {
      [CmdletBinding()]param($Value)
      if ($Value.Length -gt 2) {
       $FirstByte =  [string]::Join("",$Value[1..2])
       $SecondByte = $Value[0]
      } else {
       $FirstByte = $Value
       $SecondByte = 0
      }
      Write-Output "0x$FirstByte","0x$SecondByte"
     }

        function Convert-LargerValues {
      [CmdletBinding()]param($Value)
            if ($Value.Length -gt 4 -and $Value.Length -le 6) {
       $FirstByte =  [string]::Join("",$Value[4..5])
       $SecondByte = [string]::Join("",$Value[2..3])
                $ThirdByte = [string]::Join("",$Value[0..1])
                Write-Output "0x$FirstByte","0x$SecondByte","0x$ThirdByte"
      }
      if ($Value.Length -gt 2 -and $Value.Length -le 4) {
       $FirstByte =  [string]::Join("",$Value[2..3])
       $SecondByte = [string]::Join("",$Value[0..1])
                Write-Output "0x$FirstByte","0x$SecondByte"
      } else {
       $FirstByte = $Value
       $SecondByte = 0
                Write-Output "0x$FirstByte","0x$SecondByte"
      }
      
     }

     function Write-ProcmonFilterValue {
            [CmdletBinding()]param(
                [ValidateNotNullOrEmpty()][System.Object]$FilterObj
            )
            #Start of filter is 1 declare type as an array of bytes
            [Byte[]]$FilterRegkey = "0x1"
            #Followed by number of filters
            $NumFilters = [convert]::tostring((($FilterObj | measure-Object).count),"16")
            #Multiple registry keys can overflow when using largeValues
            #A function was created to split these large values into two bytes
            $FilterRegKey += (Convert-LargeValues -value $NumFilters)
            #Two padding bytes
            $FilterRegkey += "0x0","0x0"
            #Header is written, build filters from friendly strings
            $FilterObj | ForEach-Object {
                #Check for syntax errors
                if ($FilterRegkey -match "Error") {
                    Write-Error "Error in Write-ProcmonFilterValue: $FilterRegkey"
                } 
                #First write column code, and 9c divider
                switch($_.Column)
                {
                    "ProcessName"      {$FilterRegKey += "0x75","0x9c"}
                    "PID"              {$FilterRegKey += "0x76","0x9c"}
                    "Result"           {$FilterRegKey += "0x78","0x9c"}
                    "Detail"           {$FilterRegkey += "0x79","0x9c"}
                    "Duration"         {$FilterRegKey += "0x8d","0x9c"}
                    "ImagePath"        {$FilterRegKey += "0x84","0x9c"}
                    "RelativeTime"     {$FilterRegKey += "0x8c","0x9c"}
                    "CommandLine"      {$FilterRegKey += "0x82","0x9c"}
                    "User"             {$FilterRegKey += "0x83","0x9c"}
                    "Operation"        {$FilterRegKey += "0x77","0x9c"}
                    "ImagePath"        {$FilterRegKey += "0x84","0x9c"}
                    "Session"          {$FilterRegKey += "0x85","0x9c"}
                    "Path"             {$FilterRegKey += "0x87","0x9c"}
                    "TID"              {$FilterRegKey += "0x88","0x9c"}
                    "Duration"         {$FilterRegKey += "0x8D","0x9c"}
                    "TimeOfDay"        {$FilterRegKey += "0x8E","0x9c"}
                    "Version"          {$FilterRegKey += "0x91","0x9c"}
                    "EventClass"       {$FilterRegKey += "0x92","0x9c"}
                    "AuthenticationID" {$FilterRegKey += "0x93","0x9c"}
                    "Virtualized"      {$FilterRegKey += "0x94","0x9c"}
                    "Integrity"        {$FilterRegKey += "0x95","0x9c"}
                    "Category"         {$FilterRegKey += "0x96","0x9c"}
                    "Parent PID"       {$FilterRegKey += "0x97","0x9c"}
                    "Architecture"     {$FilterRegKey += "0x98","0x9c"}
                    "Sequence"         {$FilterRegKey += "0x7A","0x9c"} 
                    "Company"          {$FilterRegKey += "0x80","0x9c"}
                    "Description"      {$FilterRegkey += "0x81","0x9c"}
                    default            {
                         [string]$FilterRegKey = "Error: Check Column values."
                         Write-Error "$FilterRegKey"
                        }
          }
     
                #Add two zero bytes padding before comparison
                $FilterRegkey += "0x0","0x0"
                #Now add Relation byte
                switch($_.Relation)
                {
                    "is"         {$FilterRegKey += "0x0"}
                    "isNot"      {$FilterRegkey += "0x1"}
                    "lessThan"   {$filterregkey += "0x2"}
                    "moreThan"   {$FilterRegkey += "0x3"}
                    "endsWith"   {$FilterRegkey += "0x5"}
                    "BeginsWith" {$FilterRegkey += "0x4"}
                    "Contains"   {$FilterRegKEy += "0x6"}
                    "excludes"   {$FilterRegkey += "0x7"}
                    default      {
                       [string]$FilterRegKey = "Error: Check Relation values."
                       Write-Error "$FilterRegKey"
                                 }
                }
      
                #Add three zero bytes before Action (Include/Exclude)
                $FilterRegKey += "0x0","0x0","0x0"

                #Now Include/Exclude
                if ($_.Action -match "incl")     { $FilterRegkey += "0x1" }
                elseif ($_.Action -match "excl") { $FilterRegKey += "0x0" }
                else {
                    [string]$FilterRegkey = "Error: Check Action Values."
                    Write-Error "$FilterRegKey"
                }

                #Add length of <Value> string.
                #Length is hex value of (characters * 2(account for nulls) + 2)(account for spacer bytes)
                $NumPathChars = [Convert]::tostring(((($_.value.toCharArray() | measure-object).count *  2) + 2),"16")
                $FilterRegKey += (convert-LargeValues -value $NumPathChars)

                #Two zero bytes padding
                $FilterRegkey += "0x0","0x0"
                #Convert string "Value" to binary Ascii array (ie. A = 0x41)
                $_.Value.toCharArray() | ForEach-Object {
                    $FilterRegkey += (convert-largeValues -value ([Convert]::ToString(([char]$_ -as [int]),"16")))
                }
                #Current Filter calculated, pad with 10 zero bytes TTYE -- need session number at the 3rd octect or includes do not take effect for session filter
                if ($_.Column -like "PID") {
                    $hexPID = '{0:x}' -f [int]$_.Value
                    #pad the hex with leading zeros if we're an odd number
                    if ($hexPID.Length % 2 -eq 1 ) { $hexPID = $hexPID.PadLeft($hexPID.Length+1, "0") }
                    Write-Verbose "PID in hex: $HexPID"
                    $PIDEnablementValues = Convert-LargerValues -Value $hexPID
                    Write-Verbose "PID in bytes: $PIDEnablementValues"

                    #pad 2 more zero byte values...
                    $FilterRegkey += "0x0","0x0"
                    
                    #this leaves us 8 bytes to fill in with "filter enabled" values  so we need to subtract however many bytes we use from 8 and fill
                    #the rest with zeros

                    foreach ($byte in $PIDEnablementValues) {
                        $FilterRegkey += $byte
                    }
                    Write-Verbose "Filter Rules So far: $FilterRegkey"
                    $renamingZeroByte = 8 - $PIDEnablementValues.count

                    #pad with zeros
                    For ($i=0; $i -lt $renamingZeroByte; $i++) {
                        $FilterRegkey += "0x00"
                    }
                } else {
                    $FilterRegkey += "0x0","0x0","0x0","0x0","0x0","0x0","0x0","0x0","0x0","0x0"
                }
                #Check for syntax errors
                if($FilterRegkey -match "Error") {
                    Write-Error ($FilterRegkey | Sort-Object | get-unique)
                }
  
                #Set filter
                if ($env:Username -like "*$env:COMPUTERNAME*") {
                    Write-Verbose "It seems we are running under the SYSTEM account"
                    if (-not(Test-Path "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor")) {
                        New-Item "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" -Force  -ErrorVariable SetRegKeyErr | Out-Null
                    }
                    New-ItemProperty "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" "FilterRules" -Value $FilterRegKey -PropertyType Binary -Force -ErrorVariable SetRegKeyErr | Out-Null
                } else {
                    if (-not(Test-Path "HKCU:\Software\Sysinternals\Process Monitor")) {
                        New-Item "HKCU:\Software\Sysinternals\Process Monitor" -Force  -ErrorVariable SetRegKeyErr | Out-Null
                    }
                    New-ItemProperty "HKCU:\Software\Sysinternals\Process Monitor" "FilterRules" -Value $FilterRegKey -PropertyType Binary -Force -ErrorVariable SetRegKeyErr | Out-Null
                }
                if (($setRegKeyErr | Measure-Object).count -ne 0) {
                    Write-Error "Error: Writing registry failed: $SetRegKeyErr"
                }
            }
        }
    }
    process {
        #cleanUp sysinternals keys
        if ($env:Username -like "*$env:COMPUTERNAME*") {
            if (Test-Path "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor") {
                Remove-Item "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" -Recurse -Force -ErrorVariable SetRegKeyErr | Out-Null
            } 
        } else {
            if (Test-Path "HKCU:\Software\Sysinternals\Process Monitor") {
                Remove-Item "HKCU:\Software\Sysinternals\Process Monitor" -Recurse -Force  -ErrorVariable SetRegKeyErr | Out-Null
            }
        }


        $ErrorActionPreference = "Stop"
        #needed to run under the SYSTEM account
        if (-not(Test-Path HKU:\)) {
            New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | Out-Null
        }

        #Check if running as admin
        if (-not([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
            Write-Error "Error: Script must run with Admin rights."}

        #If ProcMonFolder ends with a "\", remove
        if (($ProcMonFolder[$ProcMonFolder.Length - 1]) -eq '\') {
            $ProcMonFolder.Remove((($ProcMonFolder.Length) - 1),1)
        }


 
        ##Main
        $Filters = "PID,is,$ProcessID,include"

        ##Setup running enviroment based on specified paramaters
        #Default duration to 60 seconds if not set.
        if ($Duration -eq 0) {
            $Duration = 60
        }




        if (-not($KeepAll)) {
            #Set "Drop Filtered Events"
            if ($env:Username -like "*$env:COMPUTERNAME*") {
                if (-not(Test-Path "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor")) {
                    New-Item "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" -Force -ErrorVariable SetRegKeyErr | Out-Null
                }
                    New-ItemProperty "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" "DestructiveFilter" -Value ("0x1") -PropertyType Dword -Force -ErrorVariable SetRegKeyErr | out-null
                } else {
                    if (-not(Test-Path "HKCU:\Software\Sysinternals\Process Monitor")) {
                        New-Item "HKCU:\Software\Sysinternals\Process Monitor" -Force  -ErrorVariable SetRegKeyErr | Out-Null
                    }
                    New-ItemProperty "HKCU:\Software\Sysinternals\Process Monitor" "DestructiveFilter" -Value ("0x1") -PropertyType Dword -Force -ErrorVariable SetRegKeyErr | out-null
                if (($setRegKeyErr | Measure-Object).count -ne 0) {
                    Write-Error "Error: Writing registry failed: $SetRegKeyErr"
                }
            }
        } else {
            #keep all events
            if ($env:Username -like "*$env:COMPUTERNAME*") {
                if (-not(Test-Path "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor")) {
                    New-Item "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" -Force -ErrorVariable SetRegKeyErr | Out-Null
                }
                    New-ItemProperty "HKU:\.DEFAULT\Software\Sysinternals\Process Monitor" "DestructiveFilter" -Value ("0x0") -PropertyType Dword -Force -ErrorVariable SetRegKeyErr | out-null
                } else {
                    if (-not(Test-Path "HKCU:\Software\Sysinternals\Process Monitor")) {
                        New-Item "HKCU:\Software\Sysinternals\Process Monitor" -Force  -ErrorVariable SetRegKeyErr | Out-Null
                    }
                    New-ItemProperty "HKCU:\Software\Sysinternals\Process Monitor" "DestructiveFilter" -Value ("0x0") -PropertyType Dword -Force -ErrorVariable SetRegKeyErr | out-null
                if (($setRegKeyErr | Measure-Object).count -ne 0) {
                    Write-Error "Error: Writing registry failed: $SetRegKeyErr"
                }
            }
        }
        #Convert user input into proper object
        if(($Filters.length) -ne 0) {
         Write-Verbose "Converting user supplied filter into object format."
         $FilterObj = convert-FiltertoObj -Filters $Filters
        } else {
         Write-Verbose "No filter specified, writing one that will never trigger."
         $Filters = "pid,is,AAAAAAA,exclude"
         $FilterObj = convert-FiltertoObj -Filters $Filters
        }

        #Attempt to write filter
        Write-Verbose "Attemping to write filter registry value."
        Write-ProcmonFilterValue -FilterObj $FilterObj
    
        #If no directory specified try current folder
        if(($ProcMonFolder.Length) -eq 0) {
            $ProcMonFolder = "."
        }
        Set-Location $ProcMonFolder

       
        #test the SaveLogTo directory
        if (-not(Test-Path $SaveLogTo)) {
            Write-Output "Path $SaveLogTo not found.  Attempting to create it"
            try {
                New-Item $SaveLogTo -ItemType Directory -Force -ErrorAction Stop
            }
            catch 
            {
                Write-Error "Unable to create directory `"$SaveLogTo`""
            }
        }

        #If SaveLogTo ends with a "\", remove
        if (($SaveLogTo[$SaveLogTo.Length - 1]) -eq '\') {
            $SaveLogTo = $SaveLogTo.Remove((($SaveLogTo.Length) - 1),1)
        }
        Write-Verbose "Log output directory: $SaveLogTo"


        #Cheap way to get a pseudorandom tempfile name with benifit of timestamp
        $FileDate=((get-date).TimeOfDay.ToString().Replace('.','_')).replace(':','_')
        $TempPML = "$SaveLogTo\Temp_$FileDate.pml"
        $TempCSV = ".\Temp_$FileDate.csv"

        #Test for executable
        Write-Verbose "Testing for Procmon.exe."
        if (-not(Test-Path "$ProcMonfolder\procmon.exe")) {
            Write-Error "Tested: `"$ProcMonfolder\procmon.exe`""
        }

        #Run procmon backed to file, supress prompts
        Write-Verbose "Attempting to start Process Monitor."
        start-process -filepath ".\Procmon.exe" -argument "/runtime $Duration /backingfile $TempPML /quiet /accepteula" -WindowStyle Hidden

        #Sleep for a number of seconds procmon should run
        Write-Output "Process Monitor running for $($Duration + 5) seconds."
        Write-Output "Saved the file to: `"\\$env:computername`\$($($TempPML).TrimStart(".").Replace(':','$'))`""
        Write-Output "Expected completion time: $((Get-date).AddSeconds($duration).ToLongTimeString())."

    }
}

Remote-Procmon -ProcessID $args[0] -Duration $args[1] -ProcMonFolder $args[2] -SaveLogTo $args[3] -Verbose