299 lines
10 KiB
PowerShell
299 lines
10 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Find processes that have open file handles to a specific file or directory.
|
|
|
|
.DESCRIPTION
|
|
Uses Sysinternals handle.exe (NtHandle v5.0) to find all processes holding handles
|
|
to the specified file or directory path. Supports detailed output and process information.
|
|
|
|
.PARAMETER Path
|
|
The file or directory path to search for. If not specified, prompts for input.
|
|
|
|
.PARAMETER HandleExePath
|
|
Path to handle.exe. If not specified, auto-detects from common locations.
|
|
|
|
.PARAMETER ShowDetails
|
|
Show detailed handle information including handle ID, type, and user.
|
|
|
|
.PARAMETER OutputFile
|
|
Save results to the specified file (e.g., handle.out).
|
|
|
|
.EXAMPLE
|
|
.\Find-FileHandles.ps1 -Path "D:\Project\video-av1\vav2\docs"
|
|
|
|
.EXAMPLE
|
|
.\Find-FileHandles.ps1 -Path "D:\Project\video-av1\sample\test.webm" -ShowDetails
|
|
|
|
.EXAMPLE
|
|
.\Find-FileHandles.ps1 -Path "D:\Project" -OutputFile "handle.out"
|
|
|
|
.EXAMPLE
|
|
.\Find-FileHandles.ps1 -Path "D:\Project" -ShowDetails -OutputFile "handle.out"
|
|
#>
|
|
param (
|
|
[Parameter(Position=0)]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$HandleExePath,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$ShowDetails,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$OutputFile
|
|
)
|
|
|
|
# 콘솔 인코딩 설정 (한글 깨짐 방지)
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
# Path 파라미터가 없으면 사용자에게 입력 받기
|
|
if (-not $Path) {
|
|
$Path = Read-Host "Enter the file or directory path to search for handles"
|
|
if (-not $Path) {
|
|
Write-Host "[ERROR] No path specified." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# 경로 검증 및 정규화
|
|
if (Test-Path $Path) {
|
|
$TargetPath = (Resolve-Path $Path).Path
|
|
} else {
|
|
Write-Host "[WARNING] Path does not exist: $Path" -ForegroundColor Yellow
|
|
Write-Host "Searching anyway (path might be locked or inaccessible)..." -ForegroundColor Gray
|
|
$TargetPath = $Path
|
|
}
|
|
|
|
# --- CONFIGURATION ---
|
|
# handle.exe 경로 자동 탐지
|
|
if (-not $HandleExePath) {
|
|
$possiblePaths = @(
|
|
"C:\Sysinternals\handle.exe",
|
|
"$env:ProgramFiles\Sysinternals\handle.exe",
|
|
"$env:USERPROFILE\Downloads\handle.exe",
|
|
"$env:TEMP\handle.exe",
|
|
"handle.exe" # PATH에 있는 경우
|
|
)
|
|
|
|
foreach ($path in $possiblePaths) {
|
|
if (Test-Path $path) {
|
|
$HandleExePath = $path
|
|
break
|
|
}
|
|
}
|
|
|
|
# PATH에서 handle.exe 찾기
|
|
if (-not $HandleExePath) {
|
|
try {
|
|
$HandleExePath = (Get-Command "handle.exe" -ErrorAction Stop).Source
|
|
} catch {
|
|
$HandleExePath = $null
|
|
}
|
|
}
|
|
}
|
|
# ---------------------
|
|
|
|
# handle.exe 파일 존재 여부 확인
|
|
if (-not $HandleExePath -or -not (Test-Path $HandleExePath)) {
|
|
Write-Host "[ERROR] handle.exe not found!" -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Host "Please download handle.exe from:" -ForegroundColor Cyan
|
|
Write-Host "https://learn.microsoft.com/en-us/sysinternals/downloads/handle" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host "And place it in one of these locations:" -ForegroundColor Green
|
|
@(
|
|
"C:\Sysinternals\handle.exe",
|
|
"$env:ProgramFiles\Sysinternals\handle.exe",
|
|
"Or add it to your PATH"
|
|
) | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
|
|
exit 1
|
|
}
|
|
|
|
# 타이틀 박스 동적 생성
|
|
$title = "Scanning for handles to: $TargetPath"
|
|
$boxWidth = [Math]::Max($title.Length + 4, 60)
|
|
$padding = $boxWidth - $title.Length - 2
|
|
|
|
Write-Host "╔$('═' * $boxWidth)╗" -ForegroundColor Cyan
|
|
Write-Host "║ $title$(' ' * $padding)║" -ForegroundColor Cyan
|
|
Write-Host "╚$('═' * $boxWidth)╝" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
# Execute handle.exe to get all open file handle information
|
|
$handleOutput = ""
|
|
$outputLines = @()
|
|
|
|
try {
|
|
Write-Host "[*] Executing handle.exe (this may take 10-30 seconds)..." -ForegroundColor Gray
|
|
|
|
# 진행 상황 표시를 위한 타이머
|
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
|
|
|
# handle.exe를 비동기로 실행하고 진행 표시
|
|
$job = Start-Job -ScriptBlock {
|
|
param($exePath, $targetPath)
|
|
& $exePath -accepteula -nobanner "$targetPath" 2>&1
|
|
} -ArgumentList $HandleExePath, $TargetPath
|
|
|
|
# 진행 표시
|
|
$spinnerChars = @('|', '/', '-', '\')
|
|
$spinnerIndex = 0
|
|
|
|
while ($job.State -eq 'Running') {
|
|
$elapsed = $stopwatch.Elapsed.TotalSeconds
|
|
Write-Host "`r[*] Scanning system handles... $($spinnerChars[$spinnerIndex]) [$([math]::Round($elapsed, 1))s]" -NoNewline -ForegroundColor Yellow
|
|
$spinnerIndex = ($spinnerIndex + 1) % 4
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
|
|
$stopwatch.Stop()
|
|
Write-Host "`r[✓] Scan completed in $([math]::Round($stopwatch.Elapsed.TotalSeconds, 1))s " -ForegroundColor Green
|
|
|
|
# 결과 가져오기
|
|
$handleOutput = Receive-Job -Job $job
|
|
Remove-Job -Job $job
|
|
|
|
$outputLines = $handleOutput
|
|
}
|
|
catch {
|
|
Write-Host "[ERROR] Failed to execute handle.exe: $($_.Exception.Message)" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# 결과를 저장할 배열 초기화 (ArrayList 사용으로 성능 향상)
|
|
$foundProcesses = New-Object System.Collections.ArrayList
|
|
$currentProcess = $null
|
|
|
|
# 디버깅: 출력 라인 수 표시
|
|
Write-Host "[DEBUG] Total output lines: $($outputLines.Count)" -ForegroundColor DarkGray
|
|
|
|
# Analyze handle.exe output line by line
|
|
foreach ($line in $outputLines) {
|
|
# "No matching handles found." 메시지 체크
|
|
if ($line -match "No matching handles found") {
|
|
Write-Host "✓ No processes are holding handles to this path." -ForegroundColor Green
|
|
exit 0
|
|
}
|
|
|
|
# Process line format: "processname.exe pid: 1234 type: File user: DOMAIN\User"
|
|
# 또는: "processname.exe pid: 1234 user: DOMAIN\User"
|
|
if ($line -match '^(\S+\.exe)\s+pid:\s+(\d+)\s+(.*)$') {
|
|
$processName = $matches[1]
|
|
$processId = [int]$matches[2]
|
|
$remainder = $matches[3]
|
|
|
|
# user 정보 추출
|
|
$userName = ""
|
|
if ($remainder -match 'user:\s+([^\s]+)') {
|
|
$userName = $matches[1]
|
|
}
|
|
|
|
$currentProcess = [PSCustomObject]@{
|
|
ProcessName = $processName
|
|
PID = $processId
|
|
User = $userName
|
|
Handles = New-Object System.Collections.ArrayList
|
|
}
|
|
[void]$foundProcesses.Add($currentProcess)
|
|
|
|
Write-Host "[DEBUG] Found process: $processName (PID: $processId)" -ForegroundColor DarkGray
|
|
}
|
|
# Handle detail line format: " 1A4: File (RWD) C:\path\to\file"
|
|
# 또는 간단한 형식: " 1A4: C:\path\to\file"
|
|
elseif ($line -match '^\s+([0-9A-F]+):\s+(.+)$' -and $currentProcess) {
|
|
$handleId = $matches[1]
|
|
$handleDetails = $matches[2].Trim()
|
|
|
|
# Type 정보가 있는지 확인
|
|
$handleType = "File"
|
|
if ($handleDetails -match '^(\w+)\s+(.+)$') {
|
|
$handleType = $matches[1]
|
|
$handleDetails = $matches[2].Trim()
|
|
}
|
|
|
|
$handleInfo = [PSCustomObject]@{
|
|
HandleID = $handleId
|
|
Type = $handleType
|
|
Details = $handleDetails
|
|
}
|
|
[void]$currentProcess.Handles.Add($handleInfo)
|
|
|
|
Write-Host "[DEBUG] Handle: $handleId -> $handleDetails" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
|
|
# 핸들이 없는 프로세스 제거
|
|
$processesWithHandles = New-Object System.Collections.ArrayList
|
|
foreach ($proc in $foundProcesses) {
|
|
if ($proc.Handles.Count -gt 0) {
|
|
[void]$processesWithHandles.Add($proc)
|
|
} else {
|
|
Write-Host "[DEBUG] Removing process $($proc.ProcessName) (no handles found)" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
$foundProcesses = $processesWithHandles
|
|
|
|
# 파일로 출력 (옵션)
|
|
if ($OutputFile) {
|
|
$outputLines | Out-File -FilePath $OutputFile -Encoding UTF8
|
|
Write-Host "[+] Raw output saved to: $OutputFile" -ForegroundColor Green
|
|
}
|
|
|
|
# 결과 출력
|
|
if ($foundProcesses.Count -eq 0) {
|
|
Write-Host "✓ No processes are holding handles to this path." -ForegroundColor Green
|
|
exit 0
|
|
}
|
|
|
|
Write-Host "⚠ Found $($foundProcesses.Count) process(es) with open handles:" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
|
|
foreach ($proc in $foundProcesses) {
|
|
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
|
|
Write-Host "Process: " -NoNewline
|
|
Write-Host "$($proc.ProcessName)" -ForegroundColor White -NoNewline
|
|
Write-Host " (PID: " -NoNewline
|
|
Write-Host "$($proc.PID)" -ForegroundColor Cyan -NoNewline
|
|
Write-Host ")"
|
|
|
|
if ($proc.User) {
|
|
Write-Host " User: " -NoNewline -ForegroundColor Gray
|
|
Write-Host "$($proc.User)" -ForegroundColor Yellow
|
|
}
|
|
|
|
Write-Host " Handles: " -NoNewline -ForegroundColor Gray
|
|
Write-Host "$($proc.Handles.Count)" -ForegroundColor Magenta
|
|
|
|
# 항상 핸들 상세 정보 표시
|
|
if ($proc.Handles.Count -gt 0) {
|
|
Write-Host ""
|
|
foreach ($handle in $proc.Handles) {
|
|
if ($ShowDetails) {
|
|
# 상세 모드: Handle ID와 Type도 표시
|
|
Write-Host " ├─ Handle ID: " -NoNewline -ForegroundColor DarkGray
|
|
Write-Host "$($handle.HandleID)" -ForegroundColor Cyan
|
|
Write-Host " │ Type: " -NoNewline -ForegroundColor DarkGray
|
|
Write-Host "$($handle.Type)" -ForegroundColor Green
|
|
Write-Host " │ Path: " -NoNewline -ForegroundColor DarkGray
|
|
Write-Host "$($handle.Details)" -ForegroundColor White
|
|
Write-Host " │" -ForegroundColor DarkGray
|
|
} else {
|
|
# 기본 모드: 경로만 표시
|
|
Write-Host " • " -NoNewline -ForegroundColor Yellow
|
|
Write-Host "$($handle.Details)" -ForegroundColor White
|
|
}
|
|
}
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
Write-Host "💡 To close these handles:" -ForegroundColor Yellow
|
|
Write-Host " 1. Close the applications manually" -ForegroundColor Gray
|
|
Write-Host " 2. Use Task Manager to end the processes" -ForegroundColor Gray
|
|
Write-Host " 3. Run: " -NoNewline -ForegroundColor Gray
|
|
Write-Host "Stop-Process -Id <PID> -Force" -ForegroundColor White
|
|
Write-Host "" |