This article is part of the series "Practical PowerShell for IT Security". Check out the rest:
In working on this series, I almost feel that with PowerShell we have technology that somehow time-traveled back from the future. Remember on Star Trek – the original of course — when the Enterprise’s CTO, Mr. Spock, was looking into his visor while scanning parsecs of space? The truth is Spock was gazing at the output of a Starfleet-approved PowerShell script.
Tricorders? Also powered by PowerShell.
Get the Free Pentesting Active
Directory Environments e-book
Yes, I’m a fan of PowerShell, and boldly going where no blogger has gone before. For someone who’s been raised on bare-bones Linux shell languages, PowerShell looks like super-advanced technology. Part of PowerShell’s high-tech prowess is its ability, as I mentioned in the previous post, to monitor low-level OS events, like file updates.
A Closer Look at Register-WmiEvent
Let’s return to the amazing one-line of file monitoring PS code I introduced last time.
Register-WmiEvent -Query "SELECT * FROM __InstanceModificationEvent WITHIN 5 WHERE TargetInstance ISA 'CIM_DataFile' and TargetInstance.Path = '\\Users\\bob\\' and targetInstance.Drive = 'C:' and (targetInstance.Extension = 'doc' or targetInstance.Extension = 'txt)' and targetInstance.LastAccessed > '$($cur)' " -sourceIdentifier "Accessor" -Action $action
- Register-WmiEvent -Query "SELECT * FROM __InstanceModificationEvent WITHIN 5 WHERE TargetInstance ISA 'CIM_DataFile' and TargetInstance.Path = '\\Users\\bob\\' and targetInstance.Drive = 'C:' and (targetInstance.Extension = 'doc' or targetInstance.Extension = 'txt)' and targetInstance.LastAccessed > '$($cur)' " -sourceIdentifier "Accessor" -Action $action
Register-WmiEvent -Query "SELECT * FROM __InstanceModificationEvent WITHIN 5 WHERE TargetInstance ISA 'CIM_DataFile' and TargetInstance.Path = '\\Users\\bob\\' and targetInstance.Drive = 'C:' and (targetInstance.Extension = 'doc' or targetInstance.Extension = 'txt)' and targetInstance.LastAccessed > '$($cur)' " -sourceIdentifier "Accessor" -Action $action
As you might have guessed, the logic on what to monitor is buried in the WQL contained in Register-WmiEvent
’s query parameter.
You’ll recall that WQL allows scripters to retrieve information about Windows system events in general and, specifically in our case, file events – files created, updated, or deleted. With this query, I’m effectively pulling out of Windows darker depths file modification events that are organized as a CIM_DataFile
class.
WQL allows me to set the drive and folder I’m interested in searching — that would be the Drive
and Path
properties that I reference above.
Though I’m not allowed to use a wild card search — it’s a feature, not a bug — I can instead search for specific file extensions. My goal in developing the script for this post is to help IT security spot excessive activity on readable files. So I set up a logical condition to search for files with “doc” or “txt” extensions. Makes sense, right?
Now for the somewhat subtle part.
I’d like to collect file events generated by anyone accessing a file, including those who just read a Microsoft Word documents without making changes.
Can that be done?
When we review a file list in Windows Explorer, we’re all familiar with the “Date Modified” field. But did you know there’s also a “Date Accessed” field? Every time you read a file in Windows, this field is, in theory, updated with the current time stamp. You can discover this for yourself—see below—by clicking on the column heads and enabling the access field.
However, in practice, Windows machines aren’t typically configured to update this internal field when a file is just accessed—i.e., read, but not modified. Microsoft says it will slow down performance. But let’s throw caution to the wind.
To configure Windows to always update the file access time, you use the under-appreciated fsutil utility (you’ll need admin access) with the following parameters:
fsutil set behavior disablelastaccess 0
With file access events now configured in my test environment, I’ve now enabled Windows to also record read-only events.
My final search criteria in the above WQL should make sense:
targetInstance.LastAccessed > '$($cur)'
It says that I’m only interested in file events in which file access has occurred after the Register-WmiEvent
is launched. The $cur
variable, by the way is assigned the current time pulled from the Get-Date
cmdlet.
File Access Analytics (FAA)
We’ve gotten through the WQL, so let’s continue with the remaining parameters in Register-WmiEvent
.
SourceIdentifer
allows you to name an event. Naming things – people, tabby cats, and terriers—is always a good practice since you can call them when you need ‘em.
And it holds just as true for events! There are few cmdlets that require this identifier. For starters, Unregister-Event
for removing a given event subscription, Get-Event
for letting you review all the events that are queued, Remove-Event
for erasing current events in the queue, and finally Wait-Event
for doing an explicit synchronous wait. We’ll be using some of these cmdlets in the completed code.
I now have the core of my script worked out.
That leaves the Action
parameter. Since Register-WmiEvent
responds asynchronously to events, it needs some code to handle the response to the triggering event, and that’s where the action, so to speak is: in a block of PowerShell code that’s passed in.
This leads to what I really want to accomplish with my script, and so I’m forced to reveal my grand scheme to take over the User Behavior Analytics world with a few lines of PowerShell code.
Here’s the plan: This PS script will monitor file access event rates, compare it to a baseline, and decide whether the event rates fall into an abnormal range, which could indicate possible hacking. If this threshold is reached, I’ll display an amazing dashboard showing the recent activity.
In other words, I’ll have a threat monitor alert system that will spot unusual activity against text files in a specific directory.
Will Powershell Put Security Solutions Out of Business?
No, Varonis doesn’t have anything to worry about, for a few reasons.
One, event monitoring is not really something Windows does efficiently. Microsoft in fact warns that turning on last access file updates through fsutil adds system overhead. In addition, Register-WmiEvent
makes the internal event flywheels spin faster: I came across some comments saying the cmdlet may cause the system to slow down.
Two, I’ve noticed that this isn’t real-time or near real-time monitoring: there’s a lag in receiving file events, running up to 30 minutes or longer. At least, that was my experience running the scripts on my AWS virtual machine. Maybe you’ll do better on your dedicated machine, but I don’t think Microsoft is making any kind of promises here.
Three, try as I might, I was unable to connect a file modification event to the user of the app that was causing the event. In other words, I know a file even has occurred, but alas it doesn’t seem to be possible with Register-WMIEvent
to know who caused it.
So I’m left with a script that can monitor file access but without assigning attribution. Hmmm … let’s create a new security monitoring category, called File Access Analytics (FAA), which captures what I’m doing. Are you listening Gartner?
The larger point, of course, is that User Behavior Analytics (UBA) is a far better way to spot threats because user-specific activity contains the interesting information. My far less granular FAA, while useful, can’t reliably pinpoint the bad behaviors since it aggregates events over many users.
However, for small companies and with a few account logged on, FAA may be just enough. I can see an admin using the scripts when she suspects a user who is spending too much time poking around a directory with sensitive data. And there are some honeypot possibilities with this code as well.
And even if my script doesn’t quite do the job, the even larger point is that understanding the complexities of dealing with Windows events using PowerShell (or other language you use) will make you, ahem, appreciate enterprise-class security solutions.
We’re now ready to gaze upon the Powershell scriptblock of my Register-WmiEvent
:
$action = { $Global:Count++ $d=(Get-Date).DayofWeek $i= [math]::floor((Get-Date).Hour/8) $Global:cnts[$i]++ #event auditing! $rawtime = $EventArgs.NewEvent.TargetInstance.LastAccessed.Substring(0,12) $filename = $EventArgs.NewEvent.TargetInstance.Name $etime= [datetime]::ParseExact($rawtime,"yyyyMMddHHmm",$null) $msg="$($etime)): Access of file $($filename)" $msg|Out-File C:\Users\bob\Documents\events.log -Append $Global:evarray.Add(@($filename,$etime)) if(!$Global:burst) { $Global:start=$etime $Global:burst=$true } else { if($Global:start.AddMinutes(15) -gt $etime ) { $Global:Count++ #File behavior analytics $sfactor=2*[math]::sqrt( $Global:baseline["$($d)"][$i]) write-host "sfactor: $($sfactor))" if ($Global:Count -gt $Global:baseline["$($d)"][$i] + 2*$sfactor) { "$($etime): Burst of $($Global:Count) accesses"| Out-File C:\Users\bob\Documents\events.log -Append $Global:Count=0 $Global:burst =$false New-Event -SourceIdentifier Bursts -MessageData "We're in Trouble" -EventArguments $Global:evarray $Global:evarray= [System.Collections.ArrayList] @(); } } else { $Global:burst =$false; $Global:Count=0; $Global:evarray= [System.Collections.ArrayList] @();} } }
- $action = {
- $Global:Count++
- $d=(Get-Date).DayofWeek
- $i= [math]::floor((Get-Date).Hour/8)
- $Global:cnts[$i]++
- #event auditing!
- $rawtime = $EventArgs.NewEvent.TargetInstance.LastAccessed.Substring(0,12)
- $filename = $EventArgs.NewEvent.TargetInstance.Name
- $etime= [datetime]::ParseExact($rawtime,"yyyyMMddHHmm",$null)
- $msg="$($etime)): Access of file $($filename)"
- $msg|Out-File C:\Users\bob\Documents\events.log -Append
- $Global:evarray.Add(@($filename,$etime))
- if(!$Global:burst) {
- $Global:start=$etime
- $Global:burst=$true
- }
- else {
- if($Global:start.AddMinutes(15) -gt $etime ) {
- $Global:Count++
- #File behavior analytics
- $sfactor=2*[math]::sqrt( $Global:baseline["$($d)"][$i])
- write-host "sfactor: $($sfactor))"
- if ($Global:Count -gt $Global:baseline["$($d)"][$i] + 2*$sfactor) {
- "$($etime): Burst of $($Global:Count) accesses"| Out-File C:\Users\bob\Documents\events.log -Append
- $Global:Count=0
- $Global:burst =$false
- New-Event -SourceIdentifier Bursts -MessageData "We're in Trouble" -EventArguments $Global:evarray
- $Global:evarray= [System.Collections.ArrayList] @();
- }
- }
- else { $Global:burst =$false; $Global:Count=0; $Global:evarray= [System.Collections.ArrayList] @();}
- }
- }
$action = { $Global:Count++ $d=(Get-Date).DayofWeek $i= [math]::floor((Get-Date).Hour/8) $Global:cnts[$i]++ #event auditing! $rawtime = $EventArgs.NewEvent.TargetInstance.LastAccessed.Substring(0,12) $filename = $EventArgs.NewEvent.TargetInstance.Name $etime= [datetime]::ParseExact($rawtime,"yyyyMMddHHmm",$null) $msg="$($etime)): Access of file $($filename)" $msg|Out-File C:\Users\bob\Documents\events.log -Append $Global:evarray.Add(@($filename,$etime)) if(!$Global:burst) { $Global:start=$etime $Global:burst=$true } else { if($Global:start.AddMinutes(15) -gt $etime ) { $Global:Count++ #File behavior analytics $sfactor=2*[math]::sqrt( $Global:baseline["$($d)"][$i]) write-host "sfactor: $($sfactor))" if ($Global:Count -gt $Global:baseline["$($d)"][$i] + 2*$sfactor) { "$($etime): Burst of $($Global:Count) accesses"| Out-File C:\Users\bob\Documents\events.log -Append $Global:Count=0 $Global:burst =$false New-Event -SourceIdentifier Bursts -MessageData "We're in Trouble" -EventArguments $Global:evarray $Global:evarray= [System.Collections.ArrayList] @(); } } else { $Global:burst =$false; $Global:Count=0; $Global:evarray= [System.Collections.ArrayList] @();} } }
Yes, I do audit logging by using the Out-File
cmdlet to write a time-stamped entry for each access. And I detect bursty file access hits over 15-minute intervals, comparing the event counts against a baseline that’s held in the $Global:baseline
array.
I got a little fancy here, and set up mythical average event counts in baseline
for each day of the week, dividing the day into three eight hour periods. When the burst activity in a given period falls at the far end of the “tail” of the bell curve, we can assume we’ve spotted a threat.
The FAA Dashboard
With the bursty event data held in $Global:evarray
(files accessed with timestamps), I decided that it would be a great idea to display it as a spiffy dashboard. But rather than holding up the code in the scriptblock, I “queued” up this data on its own event, which can be handled by a separates app.
Whaaat?
Let me try to explain. This is where the New-Event
cmdlet comes into play at the end of the scriptblock above. It simply allows me to asynchronously ping another app or script, thereby not tying down the code in the scriptblock so it can then handle the next file access event.
I’ll present the full code for my FAA PowerShell script in the next post. For now, I’ll just say that I set up a Wait-Event
cmdlet whose sole purpose is to pick up these burst events and then funnel the output into a beautiful table, courtesy of Out-GridView
.
Here’s the end result that will pop on an admin’s console:
Impressive in its own way considering the whole FAA “platform” was accomplished in about 60 lines of PS code.
We’ve covered a lot of ground, so let’s call it a day.
We’ll talk more about the full FAA script the next time, and then we’ll start looking into the awesome hidden content classification possibilities of PowerShell.
What should I do now?
Below are three ways you can continue your journey to reduce data risk at your company:
Schedule a demo with us to see Varonis in action. We'll personalize the session to your org's data security needs and answer any questions.
See a sample of our Data Risk Assessment and learn the risks that could be lingering in your environment. Varonis' DRA is completely free and offers a clear path to automated remediation.
Follow us on LinkedIn, YouTube, and X (Twitter) for bite-sized insights on all things data security, including DSPM, threat detection, AI security, and more.