Getting DNS Client Cached Entries with CIM/WMI

What is DNS Cache

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.  

Why is it Important

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 femoral trace on the system until the cached entry TTL (Time to Live) expires.

MSFT_DNSClientCache class

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. 

Storyboards 

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. 

Story 1

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.

Story 2

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.  

Story 3

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. 

Function Design


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:&gt; 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
        Get DNSCache entries where the name contains the string acmelabs and are for DNS record type A.    .INPUTS        Microsoft.Management.Infrastructure.CimSession    .OUTPUTS        PSGumshoe.Process    #>    [CmdletBinding(DefaultParameterSetName = “Local”)]    param (        # CIMSession to perform query against        [Parameter(ValueFromPipelineByPropertyName = $True,            ValueFromPipeline = $true,            ParameterSetName = ‘CimInstance’)]        [Alias(‘Session’)]        [Microsoft.Management.Infrastructure.CimSession[]]        $CimSession,
        # Specifies the computer on which you want to run the CIM operation. You can specify a fully qualified        # domain name (FQDN) a NetBIOS name, or an IP address.        [Parameter(ValueFromPipelineByPropertyName = $True,            ValueFromPipeline = $true,            ParameterSetName = ‘ComputerName’)]        [Alias(‘Host’)]        [String[]]        $ComputerName,
        # Name of the record. This tends to be the FQDN requested.        [Parameter(Mandatory =$false)]        [SupportsWildcards()]        [string[]]        $Name,
        # Status of the cached query, if it was a successful one or if it failed why it did.        [Parameter(Mandatory = $false)]        [ValidateSet(‘Success’, ‘NotExist’, ‘NoRecords’)]        [string[]]        $Status,
        # Type of record cached.        [Parameter(Mandatory = $false)]        [ValidateSet(‘A’, ‘NS’, ‘CNAME’, ‘SOA’, ‘PTR’, ‘MX’, ‘AAAA’, ‘SRV’)]        [string[]]        $Type,
        # Data what was the result retured. Example for A and AAAA it would be the IP and for CNAME a hostname.         [Parameter(Mandatory = $false)]        [SupportsWildcards()]        [string[]]        $Data,
        # Invert the logic for matching record, entries that match the query will be excluded.        [Parameter(Mandatory =  $false)]        [switch]        $InvertLogic    )

To 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 “Beguin” script block of the advanced function.

 begin {
        $Record2Type = @{            ‘A’ = 1            ‘NS’ = 2             ‘CNAME’ = 5             ‘SOA’ = 6             ‘PTR’ = 12             ‘MX’ = 15             ‘AAAA’ = 28             ‘SRV’ = 33        }
        $Type2Record = @{            ‘1’ = ‘A’            ‘2’ = ‘NS’             ‘5’ = ‘CNAME’            ‘6’ = ‘SOA’            ‘12’ = ‘PTR’             ‘15’ = ‘MX’            ‘28’ = ‘AAAA’             ‘33’ = ‘SRV’        }
        $Status2Val = @{            ‘Success’ = 0             ‘NotExist’ = 9003             ‘NoRecords’ = 9701        }
        $Val2Status = @{            ‘0’ = ‘Success’            ‘9003’ = ‘NotExist’            ‘9701’ = ‘NoRecords’        }
        $Section = @{            ‘1’ = ‘Answer’             ‘2’ = ‘Authority’             ‘3’ = ‘Additional’        }

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”         }
         if ($filter.Length -eq 0) {            $Wql = “SELECT  FROM MSFT_DNSClientCache"        } else {            $Wql = "SELECT  FROM MSFT_DNSClientCache WHERE $($filterLogic) $($filter -join " AND " )”        }        Write-Verbose -Message “Using WQL - $($Wql)”        

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        }
        switch ($pscmdlet.ParameterSetName) {            ‘Local’ {                 # DCOM to ensure propper connection to local host since WinRM is not garateed.                $sessop = New-CimSessionOption -Protocol Dcom                $LocalSession = New-CimSession -ComputerName $Env:Computername -SessionOption $sessop                $CimSession += $LocalSession
                # Clean session after execution.                 $CleanSession = $true
                $CimParamters.Add(‘CimSession’, $CimSession)            }
            ‘ComputerName’ {                $CimParamters.Add(‘ComputerName’, $ComputerName)            }
            ‘CimInstance’ {                $CimParamters.Add(‘CimSession’, $CimSession)            }
            Default {}        }

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 .

Article Link: https://www.darkoperator.com/blog/2020/1/14/getting-dns-client-cached-entries-with-cimwmi