Lets talk about Handle. Handle is an amazing program that allows you to see which program has access over a folder or file. This is a sysintel tool. Working with handle inside your powershell script is not a native thing. The first thing you will want to do is download handle.

We first create the folder we want handle to be downloaded in. In this case, the c:\temp folder will work. Notice we check first to see if it exists with the test path. We will continue this trend so we don’t have to go through the download and creation process repetitively.

if (!(Test-Path "c:\Temp")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }

Now we test to see if handle has already been downloaded before. If not, we download it. We are going to be downloading the handled application from http://live.sysinternals.com/handle.exe All of the Sysinternals tools are on this website and you can programmatically download them at any time. We are going to save the handle.exe in our c:\temp folder we created a few seconds ago.

if (!(Test-Path "C:\temp\handle.exe")) {
        Invoke-WebRequest -Uri "http://live.sysinternals.com/handle.exe" -OutFile "c:\temp\handle.exe"  -UseBasicParsing -DisableKeepAlive
    } 

Now we have handle, it’s time to get a handle on handle inside PowerShell. As command prompt program it needs to be called from the command prompt. We want to capture the output. Thus, using something like start-process is out of the question and here is why. Start process starts another processes. It does not keep the process in the current window. Thus you can not capture that data without doing some PowerShell magic which may or may not work. So, what we do instead is use the cmd.exe itself. We will use the /c flag and then the path to the handle software.

$ProcessHandles = cmd.exe /c C:\temp\handle.exe -a -u "$FilePath" -accepteula

Let’s break this down a little more. We are starting the handle application with an -a. The A is dumping all the handle information. This is a ton of information. The -u shows the owning user name when searching for handles. So we are grabbing all the information and the user information. We want to do this because it gives us the programs as well. Then we give it the path of the folder we want. So we basically give it a target. Now we are pulling all the handle information from a target folder with the user name/process name. The final handle flag is -accepteula. This basically makes it more automated. We call the handle using the cmd.exe /c. This brings the command output into our terminal which we can capture by placing into the $processHandles. Bam, now we have a bunch of confusing string information. The next step is to parse this string. Here is what the string looks like:

Nthandle v4.22 - Handle viewer
Copyright (C) 1997-2019 Mark Russinovich
Sysinternals - www.sysinternals.com

WINWORD.EXE        pid: 14248  type: File          ACTIVEDIRECTORY\bolding  A34: C:\temp\Change Control.docx

Now we need to handle the handle strings. So we search each string for the file name or file path with a simple where-object. This should create an array of information.

$Handles = $ProcessHandles | where-object { $_ -like "*$FilePath*" }

In this case, we only have one, but we want to make sure it doesn’t break if there is more than one. So we start a foreach loop. We loop through each handle in our handles. Each handle loops like this:

WINWORD.EXE        pid: 14248  type: File          ACTIVEDIRECTORY\bolding  A34: C:\temp\Change Control.docx

They are split apart by spaces. So, what we are going to do is use the split features. We are going to then search each line for an *.exe as most programs are .exe at the end of the day. We could expand upon this, but we will leave it here at this level. Once we have the .exe we want to remove that .exe with the replace command. Here is what the code will look like so far.

foreach ($Handle in $handles) {
        $Process = ($handle.split(' ') | where-object { $_ -like "*.exe" }) -replace '.exe', ''
}

Notice how we pipe one command into another and then wrap it with the replace. Simple one-line power right there. From here we need to test if the $process is empty. We do this because if the file in question isn’t locked down, we don’t want to error out. So a simple, if null is not equal to process, is set. The goal is to push these items into a smart system that will kill the process. However, there is one item I have discovered over the years doing this that tends to get killed by going down this route and that’s explorer.exe. I have killed it more than once. This is why I place an exclusion for explorer.exe. To do this we just check if the name matches with another if statement. So here is what the code looks like so far for this loop.

$Tasks = foreach ($Handle in $handles) {
        $Process = ($handle.split(' ') | where-object { $_ -like "*.exe" }) -replace '.exe', ''
        if ($Null -ne $process) {
            if ($Process -notlike "explorer") {
                $Process
            }
        }
    }

Now, here is the fun part. We can kill these tasks from the script itself. All we have to do is loop it through and stop each process with a stop-process. I placed a kill switch in the parameters just for this. So, if the kill switch is true, then we loop through each task killing it. If not, then we just display the processes. It’s that simple. Here is what that code looks like:

if ($kill) {
        foreach ($Task in $tasks) {
            Stop-Process -name $Task -Force
        }
    } else {
        $Tasks
    }

It’s that time, let’s put it all together and make the script.

The Script

function Set-SHDLockedFileProcess {
    param (
        [String]$FilePath,
        [switch]$kill
    )
    if (!(Test-Path "c:\Temp")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
    if (!(Test-Path "C:\temp\handle.exe")) {
        Invoke-WebRequest -Uri "http://live.sysinternals.com/handle.exe" -OutFile "c:\temp\handle.exe"  -UseBasicParsing -DisableKeepAlive
    } 
    $ProcessHandles = cmd.exe /c C:\temp\handle.exe -a -u "$FilePath" -accepteula
    $Handles = $ProcessHandles | where-object { $_ -like "*$FilePath*" }
    $Tasks = foreach ($Handle in $handles) {
        $Process = ($handle.split(' ') | where-object { $_ -like "*.exe" }) -replace '.exe', ''
        if ($Null -ne $process) {
            if ($Process -notlike "explorer") {
                $Process
            }
        }
    }
    if ($kill) {
        foreach ($Task in $tasks) {
            Stop-Process -name $Task -Force
        }
    } else {
        $Tasks
    }
}