-
Notifications
You must be signed in to change notification settings - Fork 45
/
Get Process Relatives.ps1
345 lines (296 loc) · 12.9 KB
/
Get Process Relatives.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#requires -Version 5
<#
.SYNOPSIS
Get parent and child processes details and recurse
.DESCRIPTION
Uses win32_process. Level 0 processes are those specified via parameter, positive levels are parent processes & negative levels are child processes
Child processes are not guaranteed to be directly below their parent - check process id and parent process id
.PARAMETER name
A regular expression to match the name(s) of the process(es) to retrieve.
.PARAMETER id
The ID(s) of the process(es) to retrieve.
.PARAMETER indentMultiplier
The multiplier for the indentation level. Default is 1.
.PARAMETER indenter
The character(s) used for indentation. Default is a space.
.PARAMETER unknownProcessName
The placeholder name for unknown processes. Default is '<UNKNOWN>'.
.PARAMETER properties
The properties to retrieve for each process. Pass $null or empty array to get all properties
.PARAMETER quiet
Suppresses warning output if specified.
.PARAMETER norecurse
Prevents recursion through processes if specified.
.PARAMETER noIndent
Disables creatring indented name if specified.
.PARAMETER signing
Add digital signature information for the processes executable to the output
.PARAMETER signing
Add file and version information for the processes executable to the output
.PARAMETER noChildren
Excludes child processes from the output if specified.
.PARAMETER noOwner
Excludes the owner from the output if specified which speeds up the script.
.PARAMETER sessionId
Process all processes passed via -id or -name regardless of session if * is passed (default)
Only process processes passed via -id or -name if they are in the same session as the script if -1 is passed
Only process processes passed via -id or -name if they are in the same session as the value passed if it is a positive integer
.EXAMPLE
& '.\Get Process Relatives.ps1' -id 12345
Get parent and child processes of the running process with pid 12345
.EXAMPLE
& '.\Get Process Relatives.ps1' -name notepad.exe,winword.exe -properties $null
Get parent and child processes of all running processes of notepad and winword, outputting all win32_process properties & added ones
.EXAMPLE
& '.\Get Process Relatives.ps1' -name powershell.exe -sessionid -1
Get parent and child processes of powershell.exe processes running in the same session as the script
.EXAMPLE
& '.\Get Process Relatives.ps1' -name powershell.exe -signing
Get parent and child processes of powershell.exe processes running all sessions and include digital signing detail of all processes
.NOTES
Modification History:
2024/09/13 @guyrleech Script born
2024/09/16 @guyrleech First release
2024/09/20 @guyrleech Added -signing parameter
2024/09/22 @guyrleech Made UNKNOWN object of type win32_process. Added -file for file detail including version
#>
[CmdletBinding(DefaultParameterSetName='name')]
Param
(
[Parameter(ParameterSetName='name',Mandatory=$true)]
[string[]]$name ,
[Parameter(ParameterSetName='id',Mandatory=$true)]
[int[]]$id ,
[string]$sessionId = '*' ,
[int]$indentMultiplier = 1 ,
[string]$indenter = ' ' ,
[string]$unknownProcessName = '<UNKNOWN>' ,
[object[]]$properties = @( 'IndentedName' , 'ProcessId' , 'ParentProcessId' , 'Sessionid' , '-' , 'Owner' , 'CreationDate' , 'Level' , 'Service' , 'CommandLine' ) ,
[switch]$quiet ,
[switch]$signing ,
[switch]$file ,
[switch]$norecurse ,
[switch]$noIndent ,
[switch]$noChildren ,
[switch]$noOwner
)
Function Get-DirectRelativeProcessDetails
{
Param
(
[int]$id ,
[int]$level = 0 ,
[datetime]$created ,
[bool]$children = $false ,
[switch]$recurse ,
[switch]$quiet ,
[switch]$firstCall
)
Write-Verbose -Message "Get-DirectRelativeProcessDetails pid $id level $level"
$processDetail = $null
## array is of win32_process objects where we order & search on process id
[int]$processDetailIndex = $script:processes.BinarySearch( [pscustomobject]@{ ProcessId = $id } , $comparer )
if( $processDetailIndex -ge 0 )
{
$processDetail = $script:processes[ $processDetailIndex ]
}
## else not found
## guard against pid re-use (do not need to check pid created after child process since could not exist before with same pid although can't guarantee that pid hasn't been reused since unless we check process auditing/sysmon)
if( $null -ne $processDetail -and ( $null -eq $created -or ( -not $children -and $processDetail.CreationDate -le $created ) -or $children ) )
{
## * means any session, -1 means session script is running in any other positive value is session id it process must be running in
if( $sessionId -ne '*' -and $firstCall )
{
if( $script:sessionIdAsInt -lt 0 ) ## session for script only
{
if( $processDetail.SessionId -ne $script:thisSessionId )
{
$processDetail = $null
}
}
elseif( $script:sessionIdAsInt -ne $processDetail.SessionId ) ## session id passed so check process is in this session
{
$processDetail = $null
}
}
if( $null -ne $processDetail -and $null -ne $processDetail.ParentProcessId -and $processDetail.ParentProcessId -gt 0 )
{
if( $recurse )
{
if( $children )
{
$script:processes | Where-Object ParentProcessId -eq $id -PipelineVariable childProcess | ForEach-Object `
{
Get-DirectRelativeProcessDetails -id $childProcess.ProcessId -level ($level - 1) -recurse -children $true -created $processDetail.CreationDate -quiet:$quiet
}
}
if( $firstCall -or -not $children ) ## getting parents
{
Get-DirectRelativeProcessDetails -id $processDetail.ParentProcessId -level ($level + 1) -children $false -recurse -created $processDetail.CreationDate -quiet:$quiet
}
}
## don't just look up svchost.exe as could be a service with it's own exe
[string]$service = ($script:runningServices[ $processDetail.ProcessId ]| Select-Object -ExpandProperty Name) -join '/'
$owner = $null
if( -Not $noOwner )
{
if( -Not $processDetail.PSObject.Properties[ 'Owner' ] )
{
$ownerDetail = Invoke-CimMethod -InputObject $processDetail -MethodName GetOwner -ErrorAction SilentlyContinue
if( $null -ne $ownerDetail -and $ownerDetail.ReturnValue -eq 0 )
{
$owner = "$($ownerDetail.Domain)\$($ownerDetail.User)"
}
Add-Member -InputObject $processDetail -MemberType NoteProperty -Name Owner -Value $owner
}
else
{
$owner = $processDetail.owner
}
}
## clone the process detail since may be used by another process being analysed and could be at a different level in that
## clone() method not available in PS 7.x
$clone = [CimInstance]::new( $processDetail )
Add-Member -InputObject $clone -NotePropertyMembers @{
Owner = $owner
Service = $service
Level = $level
'-' = $(if( $firstCall ) { '*' } else {''})
}
if( $signing )
{
$signingDetail = $null
if( -Not [string]::IsNullOrEmpty( $processDetail.Path ) )
{
$signingDetail = Get-AuthenticodeSignature -FilePath $processDetail.Path
}
Add-Member -InputObject $clone -MemberType NoteProperty -Name Signing -Value $signingDetail
}
if( $file )
{
$fileInfo = $null
if( -Not [string]::IsNullOrEmpty( $processDetail.Path ) )
{
$fileInfo = Get-ItemProperty -Path $processDetail.Path
}
Add-Member -InputObject $clone -MemberType NoteProperty -Name FileInfo -Value $fileInfo
}
$clone ## return
}
## else no parent or excluded based on session id
}
elseif( $firstCall ) ## only warn on first call
{
if( -not $quiet )
{
Write-Warning "No process found for id $id"
}
}
elseif( -not $quiet )
{
## TODO search process auditing/sysmon ?
$emptyResult = [CimInstance]::new( 'Win32_Process' , 'root/cimv2' )
Add-Member -InputObject $emptyResult -NotePropertyMembers @{
Name = $unknownProcessName
ProcessId = $id
Level = $level
}
if( $signing )
{
Add-Member -InputObject $emptyResult -MemberType NoteProperty -Name Signing -Value $null
}
if( $file )
{
Add-Member -InputObject $emptyResult -MemberType NoteProperty -Name File -Value $null
}
$emptyResult ## return
}
}
## main
[int]$script:thisSessionId = (Get-Process -id $pid).SessionId
$script:sessionIdAsInt = $sessionId -as [int]
class NameComparer : System.Collections.Generic.IComparer[PSCustomObject]
{
[int] Compare( [PSCustomObject]$x , [PSCustomObject]$y )
{
## cannot simply return difference directly since Compare must return int but uint32 could be bigger
[int64]$difference = $x.ProcessId - $y.ProcessId
if( $difference -eq 0 )
{
return 0
}
elseif( $difference -lt 0 )
{
return -1
}
else
{
return 1
}
}
}
## use sorted array so can find quicker
$comparer = [NameComparer]::new()
## get all processes so quicker to find parents and children regardless of session id as only filter on session id of processes specified by paramter, not parent/child
$script:processes = [System.Collections.Generic.List[PSCustomObject]]( Get-CimInstance -ClassName win32_process )
$script:processes.Sort( $comparer )
Write-Verbose -Message "Got $($script:processes.Count) processes"
## if names passed as parameter then get pids for them
if( $null -ne $name -and $name.Count -gt 0 )
{
$id = @( ForEach( $processName in $name )
{
$script:processes | Where-Object Name -Match $processName | Select-Object -ExpandProperty ProcessId
})
if( $id.Count -eq 0 )
{
Throw "No processes found for $name"
}
Write-Verbose -Message "Got $($id.Count) pids for process $name"
}
## get all services so we can quickly look them up
[hashtable]$script:runningServices = @{}
Get-CimInstance -ClassName win32_service -filter 'ProcessId > 0' -PipelineVariable service | ForEach-Object `
{
## could be multiple so store as array
$existing = $runningServices[ $service.ProcessId ]
if( $null -eq $existing )
{
$runningServices.Add( $service.ProcessId , ( [System.Collections.Generic.List[object]]$service ))
}
else ## already have this pid
{
$existing.Add( $service )
}
}
Write-Verbose -Message "Got $($script:runningServices.Count) running service pids"
[array]$results = @( ForEach( $processId in $id )
{
[array]$result = @( Get-DirectRelativeProcessDetails -id $processId -recurse:(-Not $norecurse) -quiet:$quiet -children (-Not $noChildren) -firstCall | Sort-Object -Property Level -Descending )
## now we know how many levels we can indent so the topmost process has no ident - no point waiting for all results as some may not have still existing parents so don't know what level in relation to other processes
if( -not $noIndent -and $null -ne $result -and $result.Count -gt 1 )
{
$levelRange = $result | Measure-Object -Maximum -Minimum -Property Level
ForEach( $item in $result )
{
Add-Member -InputObject $item -MemberType NoteProperty -Name IndentedName -Value ("$($indenter * ($levelRange.Maximum - $item.level) * $indentMultiplier)$($item.name)")
}
}
elseif( $null -ne $properties -and $properties.Count -gt 0 ) ## not indenting
{
$properties[ 0 ] = 'Name'
}
$result
})
if( $signing -and -not $PSBoundParameters[ 'properties' ] -and $null -ne $properties )
{
$properties += @{ name = 'Signature' ; expression = { $_.signing.Status }}
}
if( $null -eq $properties -or $properties.Count -eq 0 )
{
$results
}
else
{
$results | Select-Object -Property $properties
}