Monitoring DNS requests with PowerShell

@CyberSift we’re big fans of monitoring DNS. While there are malware campaigns out there which communicate directly with hard coded IP addresses, monitoring DNS is a good strategy for keeping tabs on what’s going on in your environment.

When it comes to monitoring windows hosts, Sysmon is an absolute must. There’s tons of information out there on how to use it, so we won’t go into it here. However, there’s a downside to using Sysmon when it comes to DNS. While Sysmon is perfectly capable of logging connections made, it relies on reverse DNS lookups to translate a given destination IP to it’s equivalent hostname:

… which is a problem for a lot of reasons, not the least of which is:

So how do we go about monitoring the actual DNS requests that have been made? The answer lies in another windows operational log:


Enabling the log via powershell is easy using the .Net EventLogConfiguration class:

$log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration 'Microsoft-Windows-DNS-Client/Operational'

Now if you open the Event Viewer you should see the
Microsoft-Windows-DNS-Client/Operational getting filled with entries. The next step is to extract and process the events we want. In this example we’ll be outputting to a csv format. The full command in all it’s glory is:

get-winevent -LogName Microsoft-Windows-DNS-Client/Operational -FilterXPath 'Event[System[EventRecordID > 0 and EventID = 3008]] and Event[EventData [Data[@Name="QueryStatus"] = 0]]' | %{([xml]$_.ToXml())} | %{ ("{0},{1}" -f $_.Event.System.Execution.ProcessID, ($_.Event.EventData.Data | Where-Object {$_.Name -eq 'QueryName' -or $_.Name -eq 'QueryResults'}  | Select-Object '#text' | ConvertTo-Csv -NoTypeInformation | Select -Skip 1 | Out-String  | %{$_.replace('"', '')} ).replace("`r`n",',')  )}

Running the above command will give output similar to the following:

The first column is the process ID, which if you’re already using Sysmon, makes it easy to correlate which process actually fired of the DNS request. The subsequent column in the DNS request followed by the answer.

The query works for IPv4, IPv6 and cached DNS queries. Now, let’s break down the command. The powershell command can be broken down into a sequence, each sequence pipes it’s output to the next sequence in the chain. Starting from the first sequence:

get-winevent -LogName Microsoft-Windows-DNS-Client/Operational -FilterXPath 'Event[System[EventRecordID > 0 and EventID = 3008]] and Event[EventData [Data[@Name="QueryStatus"] = 0]]'

This is where we query the log and apply a filter to get only those events we’re interested in. The XPath filter specifies the following conditions:

  • EventRecordID should be greater than 0 (we use this so that in subsequent runs of the command we only get newer events by replacing 0 with the record ID of the last event we got
  • EventID should be 3008 (DNS Query has finished processing)
  • The Query Status returned should be 0 (no errors). N.B. : thinking about this, you may want to monitor even queries that return an error – this is a common scenario in fast-flux domain malware

The next sequence in the command is:


If you go into the “Details” tab of an entry in the event viewer, you’ll be able to see the event in XML format, which contains all the information we’d like to extract. So we convert each event log entry into it’s XML equivalent using the above command. Some notes to help you understand the above:

  • %{} is used by PowerShell as shorthand instead of a “For Each” loop
  • $_ is used by PowerShell as shorthand for the current object in a loop (sort of like a loop “this”)
  • [xml] makes sure we cast to a PowerShell XML object

At this stage we have the individual XML objects making up our event stream. Now we need to extract just the info we want. In fact, the rest of the command is just wrestling the unwieldy XML to extract the data we want. The next sequence in the command is in fact itself a series of subsequences, the first of which is:

"{0},{1}" -f $_.Event.System.Execution.ProcessID, (...)

We’re printing a string and substituting the placeholders with the process ID and the rest of the command which we’ll explain shortly. The process ID is easy to extract since within the XML it has it’s own element. Unfortunately this is not the case with the Query, which is encapsulated within the “Data” element, just having different “name” attribute:

So within those brackets we try to extract just the QueryName and the QueryResults:

$_.Event.EventData.Data | Where-Object {$_.Name -eq 'QueryName' -or $_.Name -eq 'QueryResults'}

Note we query the the XML for all “Data” nodes, where the “name” attribute is either QueryName or QueryResults. We’re also only interested in the actual text within the node, not the metadata, so we filter the output using:

Select-Object '#text'

Now we can convert the PowerShell Object to a string using:

ConvertTo-Csv -NoTypeInformation | Select -Skip 1 | Out-String

(Aside: there probably is a better way of doing this that doesn’t require outputting to CSV, and hence makes the below redundant…) The above command converts the object to CSV, stripping out the metadata and the first row which contains header information. Unfortunately, while this works, the output is now on two seperate lines since QueryResults and QueryStatus are actually two objects. This means the output is not comma separated as required by CSV… so we need to make a quick search and replace to change the extra newlines with a comma, and while we’re at it remove some double quotes:

 %{$_.replace('"', '')} ).replace("`r`n",',')

And that’s about it!