Share via


Recovering Static DNS Records from a DIT Snapshot

Suppose you have the – quite common – scenario where your AD-integrated DNS zone contains many manually created (static) A records.

One day you find that many of these records were deleted by mistake. You have no file-level backup of the zone, or any other documentation of the static records that existed on it. All you have is a system state backup of a DC that is running DNS and has a copy of the zone. Also, for some reason you have not enabled the AD Recycle Bin yet.

Instead of recovering the entire zone from the backup you want to extract the list of the static A records so you can add them back manually.

In this post I will show how to use a DIT snapshot with some of the Active Directory PowerShell Module cmdlets to extract the static A records from backup.

NOTE: The procedure shown in this post is intended to explain how the DNS records are stored in an Active Directory integrated zone. The procedure is not intended to replace any disaster recovery processes. The steps in this post are informational only and they should not be used as a replacement of a DNS disaster recovery plan.

The Scenario

Lets start by defining our scenario:

  • Single-domain forest named contoso.com
    • The AD Recycle Bin feature is not enabled in this forest
  • CONTOSODC1 is a Windows Server 2008 R2 DC running DNS server
  • The zone contoso.com is AD-integrated and stored in the DomainDNSZones application partition for contoso.com
  • Several static A records were created in the zone contoso.com
  • A recent system state backup exists for CONTOSODC1; this backup was taken using Windows Server Backup and the backup was stored in an additional hard disk in the same DC

Getting Access to the DIT from the Backup

Windows Server Backup (WSB) stores the content of a backup in VHDs files. One VHD file exists for each partition that had data to include in the backup, so for example, if the server being backed-up has two partitions (C: and D:) that have data to include in the backup, then two VHDs will be generated by WSB, one for each partition (if the server has a System Reserved partition, then a VHD will be created for this partition too).

In the case of a system state backup of a DC, one of the VHDs generated by WSB will contain the snapshot of the DIT taken during the backup. This would be the VHD that corresponds to the partition where the DIT was originally stored in the server.

The VHDs generated by WSB can be mounted to access the data from the backup, meaning that we can access the snapshot of the DIT without the need to restore the backup (for more information on the folder structure created by WSB check this). In general, the VHD file will be stored in a folder structure like this:

<DRIVE>:\WindowsImageBackup\<COMPUTERNAME>\Backup <DATE> <TIME>

where:

  • <DRIVE> is the drive letter of the partition where the backup is being stored
  • <COMPUTERNAME> is the name of the computer that is being backed-up
  • <DATE> is the date when the backup was taken
  • <TIME> is the UTC time when the backup was taken

To mount the VHD we can use the Disk Management MMC (diskmgmt.msc) or DISKPART. We are going to use dskmgmt.msc to mount the VHD.

In diskmgmt.msc select the Attach VHD option in the Action Menu:

image

Then browse to the location where the VHDs were saved by WSB and select the VHD for the partition that contained the DIT. Take note of the drive letter assigned to the mounted VHD.

NOTE: be careful to not modify the content of the VHD or you might damage your backup.

Mounting the DIT Snapshot

Once we got access to the VHD with the snapshot, we are going to use DSAMAIN to mount it. In this case we are going to use LDAP port 7777. In our example the VHD was mounted with drive letter F:, and the DIT is in the default location \WINDOWS\NTDS:

image

After the snapshot is mounted, we can verify that we can access the content of the DIT by using ldp.exe and connecting to the port that we chose for the instance:

image

And browse the content of the database and our DNS zone in the backup:

image

Understanding how DNS Data is Stored in AD

The information on how DNS data is stored in AD can be found in [MS-DNSP]: Domain Name Service (DNS) Server Management Protocol. We are going to use the information that is relevant for this case.

Each AD-integrated zone is stored as a container named as the zone with object class dnsZone. The location of the container can be in an application partition or a domain partition. In the case of an application partition the zone will be inside the MicrosoftDNS container, in the case of a domain partition the zone will be stored under System/MicrosoftDNS.

Each name registered in the zone is stored as an object of class dnsNode and with the name attribute having the same value as the entry it represents in DNS. The data about the DNS name (like the IPV4 address or the MX entry value) is stored in an multi-value attribute named dnsRecord on this object.

So, for example, the information for the record CONTOSOSRV1 will be stored in an object of class dnsNode and with name CONTOSOSRV1:

image

Now, a single DNS name can map to more than one data element. For example, the name CONTOSOSRV1 can be mapped to two IPV4 addresses. Or the same name can map to an IPV4 address and an IPV6 address. In this case there will be only one dnsNode object with the name CONTOSOSRV1 and its dnsRecord attribute will contain multiple entries (remember that it is a multi-value attribute), one for each element data. If you use ADSIEDIT.msc to check the dnsRecord attribute for a dnsNode object with multiple data elements you are going to see something like this (each byte is separated by the “\” symbol and the values are shown in hexadecimal):

image

In this case the same name has been mapped to two different IPV4 addresses, and the dnsRecord attribute shows two entries. This means that to extract the data about an specific name we have to check each of the entries in the dnsRecord attribute.

The dnsRecord attribute is defined in the schema as an Octet String, meaning that it is a sequence of bytes (or a BLOB). The format of the entries in the dnsRecord attribute is defined here:

image

Without going into too much details about this structure, lets get the fields that are important for our case:

  • Byte 0 and 1 should be interpreted as a single 16-bit word that contains the length (in bytes) of the Data field.
    • For our case the entry should have the value 4 for an IPV4 address.
  • Byte 2 and 3 should be interpreted as a single 16-bit word that contains the type of the record.
    • For an IPV4 address (record type “A” in DNS), the value should be 1.
  • Byte 20 to 23 should be interpreted as a single 32-bit word that contains the timestamp of the record.
    • For static records this should have the value 0.
  • For a record of type “A”, Byte 24 will contain the first byte of the IPV4 address, Byte 25 the second, Byte 26 the third and Byte 27 the fourth. Note that the notation here is the binary notation of the IPV4 address and not the conventional “dotted” notation.

ADVANCED STUFF: you might wonder the reason why the first byte (byte 0) in an entry in dnsRecord for an IPV4 address has the value 4 (like in the previous screenshot that shows the values in the multi-valued attribute separated by the “\” symbol). Considering that this byte is part of a 16-bit word that defines the length of the data, you would expect that the first byte is a 0 and the second byte a 4 for a 16-bit word with the value 4 (or 0x0004 in hex). The reason is that the information in the BLOB is stored in little-endian format, which means that a multi-byte word is stored starting with the least-significant byte and ending with the most-significant byte. In little-endian format, the 16-bit value 0x0004 is stored as a byte with 0x04 followed by a byte with 0x00.

Aimed with this information we can build a script that iterates over the dnsNode objects in the container for the zone, for each one of these objects verifies the value of each of the entries on the dnsRecord attribute, and for each of the entries that are detected as a static IPV4 address output the name and IPV4 address.

Extracting the Static A Records from the Snapshot

Here is a pseudo-code of what we want to do:

  • FOREACH dnsNode object in the container of the zone taken from the snapshot instance
    • FOREACH dnsRecord attribute of the current dnsNode object
      • IF (Data length is 4) and (Record Type is 1) and (Timestamp is 0)
        • Build the IPV4 address as the concatenation of bytes 24 to 27 using a “.”
        • Output the name and IPV4 address

NOTE: I am going to use the PowerShell commands in iterative way instead of using the pipeline to make the script easier to understand.

To get the dnsNode objects from the zones container we are going to use the Get-ADObject cmdlet with the following parameters

  • -Filter 'objectClass -eq "dnsNode"' to indicate that the objects that we want to be returned should be of the class dnsNode.
  • -SearchBase 'dc=contoso.com,cn=MicrosoftDNS,dc=DomainDNSZones,dc=contoso,dc=com' to indicate that we want objects stored inside this container. This is the container for the zone contoso.com in our example scenario.
  • -Property 'dnsRecord' to indicate that we want the attribute dnsRecord returned with the results (by default this attribute is not returned).
  • -Server 'contosodc1:7777' to indicate that we want to query the instance that is running the DIT snapshot instead of the “current” AD instance (remember that in the previous step we mounted the DIT with DSAMAIN and asked to listen for LDAP on port 7777).

To verify the values of the entries in the dnsRecord BLOB we are going to use the bitconverter .NET class. This class allows us to convert from a sequence of bytes to words of specific sizes. The static functions of this class that we are interested in are:

  • ToUInt16(byte[], index) : to convert the 2 bytes starting at index in the byte array to a 16-bits unsigned word
  • ToUInt32(byte[], index) : to convert the 4 bytes starting at index in the byte array to a 32-bits unsigned word

Remember that in our case:

  • The length starts at index 0 and is 2 bytes long
  • The record type starts at index 2 and is 2 bytes long
  • The timestamp starts at index 20 and is 4 bytes long

Finally, we need a way to build the “dotted” notation of an IPV4 address from the dnsRecord entry. Remember that for this type of record the IPV4 address starts in byte 24 and ends in byte 27. We are going to use the –join PowerShell operator to concatenate the characters in an array using a delimiter.

And here is the final script to recover the static A record in our example scenario:

# Get the dnsNode objects in the zone container
$DNSNodes = Get-ADObject -Filter 'objectClass -eq "dnsNode"' -SearchBase 'dc=contoso.com,cn=MicrosoftDNS,dc=DomainDNSZones,dc=contoso,dc=com' -Property 'dnsRecord' -Server 'contosodc1:7777'

# Iterate on the list of returned dnsNode objects
foreach ($n in $DNSNodes)
  {
    # Ignore the "@" or (same as parent) entries
    if ($n.name -eq "@")
    {
        continue    
    }
    # A single name can have multiple IPV4 static addresses, we are going to store each static IPV4 address found in an array    
    # Initialize the array as empty    
    $IPV4 = @()
   
    # Iterate over the values in the dnsRecord attribute of the current dnsNode object
    foreach ($r in $n.dnsRecord)
    {    
        # Verify if the current entry in the dnsRecord attribute is for a static IPV4 record
        if (([bitconverter]::ToUInt16($r, 0) -eq 4) -and ([bitconverter]::ToUInt16($r, 2) -eq 1) -and ([bitconverter]::ToUInt32($r, 20) -eq 0))
        {
            # A static IPV4 address was detected, generate the "dotted" notation of the address and add it to the array
            $IPV4 += ($r[24..27] -join '.')
        }
    }

    # Ignore names that do not have any static IPV4 address
    if ($IPV4.Length -gt 0)
    {
        # Output the information found as a custom object. This will allow the output of this script to be used in the PS pipeline
        # The custom object will have two properties:
        #    name contains the name of the record
        #    IPV4address is an array with all the static IPV4 address found for this name
        New-Object -TypeName psobject -Property @{Name=$n.name;IPV4Address=$IPV4}
    }
  }

Hope you find this information useful.

[Thanks to Yusuf Dikmenoglu for his feedback and review of this post]