The DNS cache maintains a database of recent DNS resolution in memory. This allows for faster resolution of hosts that have been queried in the recent past. To keep this cache fresh and reduce the chance of stale records the time of items in the cache is of 1 day on Windows clients.
The DNS Client service in Windows is the one that manages the cache on a system, This time Window can be modified via the registry in the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters where the MaxCacheTtl property controls the time in the cache in seconds and the MaxNegativeCacheTtl property controls the time a failed response is cached.
For an attacker, it means primarily situational awareness. It allows him to know what other systems this host has accessed and the IP address of the host. This may allow identifying security platforms by the FQDNs used as well as business process systems, both internal or in the cloud. On an important note for the attacker is that if his implant/agent on the system does not include its own resolution capability it has an IOC present on the system that can be used to track its command and control infrastructure.
For a defender, the ability to know what hosts a system may have connected to in the last 24 hours. This will permit a defender to query across his environment for hosts that are communicating or have communicated with a specific host if DNS resolution was part of the process and if the attacker is not using its own resolution method. If the attacker is “Living off the Land” and using OS tools it will still leave the ephemeral trace on the system until the cached entry TTL (Time to Live) expires.
In Windows 8/2012 Microsoft added the MSFT_DNSClientCache class into the CIM object database in Windows. The class is under the new namespace that was also added to Root\StandardCimv2 and the resources are provided as part of the DnsClientCim.dll. This allows us to query for instances of the class and get all entries for the DNS Cache database.
We can query using the Get-CimInstance cmdlet in PowerShell.
Get-CimInstance -Namespace root/StandardCimv2 -ClassName MSFT_DNSClientCache
Each instance object is defined in the Managed Object Format (MOF) file DnsClientCim.mof and we can read the properties and methods it offers in the Microsoft documentation page https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/hh872334(v%3Dvs.85).
For both attacker and defender, this means we have a way to query the DNS Cache without the use of command-line tools like ipconfig.exe or having to code against the Win32 API using the DnsQuery function, called with the DNS_QUERY_NO_WIRE_QUERY query option, simplifying the code we need write. Also by being in the CIM Object database, it means we can query it remotely using Windows Remote Management (WinRM) or Windows Management Instrumentation (WMI) against remote systems.
For leveraging this function say for threat hunting a process that I learned when managing multiple development teams was the importance of proper storyboarding so as to have a clearer set of goals and logic paths when starting to develop a piece of software. I kept them simple in my head while I coded a function for PSGumshoe a PowerShell module I put functions useful for when I’m looking at IOCs and for threat hunting.
A threat hunter should be able to leverage CIM Sessions so as to make it easier to automate more than one function when hunting in large environments. This will also allow him to select RPC or WinRM depending on which option is available and better suits my needs.
A threat hunter should be able to filter on record queried and result both exact and partial. For the query, he should be able to specify one or more records.
The output from the execution of the function should be an abject so it can be leveraged in Out-GridView and any other cmdlet that will allow for filtering on converting the information into other formats for consumption in other tools.
To know what are going to be the parameters of the function I need to know the names of properties that are populated and I can work with. Many times even if an object is defined in the CIM Database to have a specific property and documentation references what data it will have you will find in production that they are left empty and not used.
A good starting point is to select all properties and get all instances to see what fields are not populated and what type of information is stored on each.
PS C:\> Get-CimInstance -Query "SELECT * FROM MSFT_DNSClientCache" -Namespace root/StandardCimv2
Caption :
Description :
ElementName :
InstanceID :
Data : 10.120.120.2
DataLength : 4
Entry : dc1.acmelabs.pvt
Name : dc1.acmelabs.pvt
Section : 1
Status : 0
TimeToLive : 639
Type : 1
PSComputerName :
....
As can be seen Caption, Description, ElementName, InstanceID, and PSCompter are empty. A good practice is to see if the behavior is the same when querying remotely. A CIMSession is created against the local host to simulate this.
PS C:\> $cimsopt = New-CimSessionOption -Protocol Dcom
PS C:\> New-CimSession -ComputerName $env:COMPUTERNAME -SessionOption $cimsopt
Id : 1
Name : CimSession1
InstanceId : cfbf740d-9434-46ab-8390-c203161cc01b
ComputerName : CL01
Protocol : DCOM
When a CIMSession is used we see the PSComputer property now has the computer name value. This is important since the function will be used for threat hunting, even if ran locally it is important to have that field populated so in later inspection have an idea from what host that information was collected from.
PS C:\> $sessions = Get-CimSession
PS C:\> Get-CimInstance -Query "SELECT * FROM MSFT_DNSClientCache" -Namespace root/StandardCimv2 -CimSession $sessions
Caption :
Description :
ElementName :
InstanceID :
Data : 40.69.222.109
DataLength : 4
Entry : array613.prod.do.dsp.mp.microsoft.com
Name : array613.prod.do.dsp.mp.microsoft.com
Section : 1
Status : 0
TimeToLive : 130
Type : 1
PSComputerName : CL01
Caption :
Description :
ElementName :
InstanceID :
Data : 40.69.216.73
DataLength : 4
Entry : array611.prod.do.dsp.mp.microsoft.com
Name : array611.prod.do.dsp.mp.microsoft.com
Section : 1
Status : 0
TimeToLive : 1740
Type : 1
PSComputerName : CL01
Caption :
Description :
ElementName :
InstanceID :
Data : config.teams.trafficmanager.net
DataLength : 8
Entry : config.teams.microsoft.com
Name : config.teams.microsoft.com
Section : 1
Status : 0
TimeToLive : 45
Type : 5
PSComputerName : CL01
Since the properties we are not interested in are empty there is no need to only select those we want so as to reduce traffic and memory usage when querying large numbers of hosts. One important thing to remember is that in WQL there is a difference in wildcard matching where in most languages the * is used in WQL the percentage symbol % is used.
PS C:\> $WQL = "SELECT * FROM MSFT_DNSClientCache WHERE Name like '%acmelabs%'"
PS C:\> Get-CimInstance -Query $WQL -Namespace root/StandardCimv2 -CimSession $sessions
Caption :
Description :
ElementName :
InstanceID :
Data : 10.120.120.2
DataLength : 4
Entry : dc1.acmelabs.pvt
Name : dc1.acmelabs.pvt
Section : 1
Status : 0
TimeToLive : 3019
Type : 1
PSComputerName : CL01
PS C:\Users\cperez> Measure-Command -Expression {$results = Get-CimSession | Get-CimInstance win32_bios} Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 588 Ticks : 5887134 TotalDays : 6.8138125E-06 TotalHours : 0.0001635315 TotalMinutes : 0.00981189 TotalSeconds : 0.5887134 TotalMilliseconds : 588.7134 PS C:\Users\cperez> Measure-Command -Expression {$results = Get-CimInstance win32_bios -CimSession (Get-CimSession)} Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 236 Ticks : 2361258 TotalDays : 2.7329375E-06 TotalHours : 6.55905E-05 TotalMinutes : 0.00393543 TotalSeconds : 0.2361258 TotalMilliseconds : 236.1258
Now since the query is a single class and I will not be doing additional queries for reasons of speed I want to use the internal multithreading of the CIM functions by passing a collection to it and not simply go CIM connection after CIM connection making querying multiple hosts a serial action.
If we look at a simple request against 100 CIM Sessions for BIOS information we can see the difference in speed. I will leverage this in function by processing all CIMSessions at once and just processing the resulting objects one by one. In functions with multiple queries on a host or leveraging associations to other classes, this would not be possible this way.
For the function, I will leverage parameter sets to differentiate the unique parameters for when it is running against the local system, against a collection of computers or against a collection of CIMSessions.
function Get-CimDnsCache { <# .SYNOPSIS Get DNS Cache entries for Windows 8/2012 or above systems leveraging CIM. .DESCRIPTION Get DNS Cache entries for Windows 8/2012 or above systems leveraging CIM. .EXAMPLE PS C:\> Get-CimDNSCache -Name *acmelabs* -Type ATo simplify the conversions of the types provided in the parameters and to turn it into values in properties of the objects generated by function so it is easier to understand. Since these values should be declared only once in the pipeline they are placed in the “Begin” script block of the advanced function.
begin {For filtering the function is designed for looking at more than one value and allow for the combination of filters as specified in the stories. For this in the begin block logic is added to create the WQL WHERE filter that will allow for collections of exact matches and also for use of wildcards. This filter is only needed once and re-used against each CIMSession passed through the pipeline the filter is only created once in the begin block.
# Build WQL Query $PassedParams = $PSBoundParameters.Keys $filter = @() switch ($PassedParams) { "Name" { $nFilter = @() foreach($n in $name){ if ($n -match "\*") { $nfilter += "Name LIKE '($n.Replace('*','%'))'" } else { $nfilter += "Name = '$($n)'" } } $filter += "($($nfilter -join " OR "))" } "Data" { $dataFilter = @() foreach($d in $Data){ if ($n -match "\*") { $dataFilter += "Data LIKE '($d.Replace('*','%'))'" } else { $dataFilter += "Data = '$($d)'" } } $filter += "($($dataFilter -join " OR "))" } "Status" { $sFilter = @() foreach($s in $Status){ $sFilter += "Status = $($Status2Val."$($s)")" } $filter += "($($sFilter -join " OR "))" } "Type" { $tFilter = @() foreach($t in $Type){ $tFilter += "Type = $($Record2Type."$($t)")" } $filter += "($($tFilter -join " OR "))" } Default {} } $filterLogic = '' if ($InvertLogic) { $filterLogic = "NOT" }To simplify execution and reduce repetitive code the parameters are checked and the cmlet parameters are defined in a HasTable that is then passed once.
$CimParamters = @{ 'Namespace' = 'root/StandardCimv2' 'Query' = $Wql }The parameter hashtable is passed to the cmdlet and a custom object is created with properties with values that are easier to understand what they relate to in each DNS Cache entry.
Get-CimInstance @CimParamters | ForEach-Object { $objprops = [ordered]@{} $objprops.add('Name',$_.name) $objprops.add('Entry',$_.Entry) $objprops.add('Data',$_.Data) $objprops.add('DataLength',$_.DataLength) $objprops.add('Section',$Section."$($_.Section)") $objprops.add('Status', $Val2Status["$($_.Status)"]) $objprops.add('TimeToLive',$_.TimeToLive) $objprops.add('Type', $Type2Record["$($_.Type)"]) $objprops.add('ComputerName',$_.PSComputerName) $obj = [PSCustomObject]$objProps $obj.pstypenames.insert(0,'PSGumshoe.DNSCacheEntry') $obj }each object is sent out into the pipeline in the process block. This will ensure proper use of the pipeline when trying in the function with other PowerShell advanced functions or cmdlets.
One part that needs to be taken care of is if it was running locally and a temporary CIMSession was created to clean the session after execution. This is done in the “End” block of the advanced function.
end { if ($CleanSession) { Remove-CimSession -CimSession $LocalSession } }Now the function can be leveraged to hunt and filter across one or multiple hosts leveraging CIMSessions and allow for the
PS C:\> Get-CimDNSCache -Name *acmelabs* -Type A
Name : dc1.acmelabs.pvt
Entry : dc1.acmelabs.pvt
Data : 10.120.120.2
DataLength : 4
Section : Answer
Status : Success
TimeToLive : 1774
Type : A
ComputerName : CL01
One of the main reasons for leveraging CIMSessions is that we can have a collection that is a mix of DCOM and WinRM connections and simplifies the authentication to the hosts.
The function is part of the PSGumshoe PowerShell Module https://github.com/PSGumshoe/PSGumshoe/blob/master/CIM/Get-CimDNSCache.ps1 .
Click to Open Code Editor