Getting the Contents of an Active Directory Integrated DNS Zone

Microsoft has long offered (where "long" means "since Windows Server 2003") the "dnscmd.exe" program to control the actions of a DNS server. And, if you installed the adminpack (RSAT-Tools-DNS on Vista/Server 2008 and above) you could perform this control on a remote workstation/server as well.

However, the output of dnscmd leaves....something.... to be desired, especially when you want to format and manipulate that output.

Using a prior blog post of mine (Hex and Decimal Output In PowerShell), I more-or-less reverse-engineered the format of how Active Directory Integrated zones (DNS domains) are stored in Active Directory. For all of my personal testing, the program below was successful at displaying the output and decoding it properly. However, that doesn't mean I decode everything!!! Just those things I could test.

A key desire of mine was to be able to get the contents of an ADI zone into a CSV (comma-separated-value) format; so of course that option is present. I also wanted proper timeout values for aging/scavenging, so of course that happens by default (which dnscmd can't do at all).

If the script below doesn't work for you, I'd be interested in hearing about it, and why it doesn't work. Of course, I make no promises, but I'll probably fix those issues. :-)

Without further ado...

##
## dns-dump.ps1
##
## Michael B. Smith
## michael at smithcons dot com
## http://TheEssentialExchange.com/blogs/michael
## May/June, 2009
##
## Use as you wish, no warranties expressed, implied or explicit.
## Works for me, but it may not for you.
## If you use this, I would appreciate an attribution.
##

Param(
	[string]$zone,
	[string]$dc,
	[switch]$csv,
	[switch]$help
)

function dumpByteArray([System.Byte[]]$array, [int]$width = 9)
{
	## this is only used if we run into a record format
	## we don't understand.

	$hex = ""
	$chr = ""
	$int = ""

	$i = $array.Count
	"Array contains {0} elements" -f $i
	$index = 0
	$count = 0
	while ($i-- -gt 0)
	{
		$val = $array[$index++]

		$hex += ("{0} " -f $val.ToString("x2"))

		if ([char]::IsLetterOrDigit($val) -or 
		    [char]::IsPunctuation($val)   -or 
		   ([char]$val -eq " "))
		{
			$chr += [char]$val
		}
		else
		{
			$chr += "."
		}

		$int += "{0,4:N0}" -f $val

		$count++
		if ($count -ge $width)
		{
			"$hex $chr $int"
			$hex = ""
			$chr = ""
			$int = ""
			$count = 0
		}		
	}

	if ($count -gt 0)
	{
		if ($count -lt $width)
		{
			$hex += (" " * (3 * ($width - $count)))
			$chr += (" " * (1 * ($width - $count)))
			$int += (" " * (4 * ($width - $count)))
		}

		"$hex $chr $int"
	}
}

function dword([System.Byte[]]$arr, [int]$startIndex)
{
	## convert four consecutive bytes in $arr into a
	## 32-bit integer value... if I had bit-manipulation
	## primitives in PowerShell, I'd use them instead
	## of the multiply operator.

	$res = $arr[$startIndex]
	$res = ($res * 256) + $arr[$startIndex + 1]
	$res = ($res * 256) + $arr[$startIndex + 2]
	$res = ($res * 256) + $arr[$startIndex + 3]

	return $res
}

function analyzeArray([System.Byte[]]$arr, [System.Object]$var)
{
	$nameArray = $var.distinguishedname.ToString().Split(",")
	$name = $nameArray[0].SubString(3)

	## AGE is stored backwards. The most-significant-byte comes
	## last, instead of first, unlike all the other 32-bit values.
	$age = $arr[23]
	$age = ($age * 256) + $arr[22]
	$age = ($age * 256) + $arr[21]
	$age = ($age * 256) + $arr[20]
	if ($age -ne 0)
	{
		## hours since January 1, 1601 (start of Windows epoch)
		## there is a long-and-dreary way to do this manually,
		## but get-date makes it trivial to do the conversion.
		$timestamp = (get-date -year 1601 -month 1 -day 1 -hour 0 -minute 0 -second 0).AddHours($age)
	}

	$ttl = dword $arr 12

	if ($arr[0] -eq 4 -and $arr[1] -eq 0)
	{
		# "A" record
		$ip = "{0}.{1}.{2}.{3}" -f $arr[24], $arr[25], $arr[26], $arr[27]

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "A", $ip
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "A", $ip
		}
	}
	elseif ((($arr[0] -eq 80) -or ($arr[0] -eq 82)) -and $arr[1] -eq 0)
	{
		# "SOA" record
		# "Start-Of-Authority"

		$nslen = $arr[44]
		$segments = $arr[45]
		$index = 46
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		$priserver = $nsname
		# "Primary server: $nsname"

		$index += 1
		$nslen = $arr[$index++]
		$segments = $arr[$index++]

		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		# "Responsible party: $nsname"
		$resparty = $nsname

		#"TTL: $ttl"
		# "Age: $age"

####		$unk1 = dword $arr 16
####		"Unknown1: $unk1"

		$serial = dword $arr 24
		# "Serial: $serial"

		$refresh = dword $arr 28
		# "Refresh: $refresh"

		$retry = dword $arr 32
		# "Retry: $retry"

		$expires = dword $arr 36
		# "Expires: $expires"

		$minttl = dword $arr 40
		# "Minimum TTL: $minttl"

		if ($age -eq 0)
		{
			$agestr = "[static]"
		}
		else
		{
			$agestr = "[" + $timestamp.ToString() + "]"
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10}"

			$formatstring -f $name, $agestr, $ttl, `
				"SOA", $priserver, $resparty, `
				$serial, $refresh, $retry, `
				$expires, $minttl
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}"

			$formatstring -f $name, $agestr, $ttl, "SOA"
			(" " * 32) + "Primary server: $priserver"
			(" " * 32) + "Responsible party: $resparty"
			(" " * 32) + "Serial: $serial"
			(" " * 32) + "TTL: $ttl"
			(" " * 32) + "Refresh: $refresh"
			(" " * 32) + "Retry: $retry"
			(" " * 32) + "Expires: $expires"
			(" " * 32) + "Minimum TTL (default): $minttl"
		}

		#### dumpByteArray $arr
	}
	elseif ((($arr[0] -eq 32) -or ($arr[0] -eq 30)) -and $arr[1] -eq 0)
	{
		# "NS" record
		$nslen = $arr[24]
		$segments = $arr[25]
		$index = 26
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "NS", $nsname
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "NS", $nsname
		}
	}
	elseif ((($arr[0] -eq 36) -or ($arr[0] -eq 38)) -and $arr[1] -eq 0)
	{
		# "SRV" record

		$port = $arr[28]
		$port = ($port * 256) + $arr[29]

		$weight = $arr[26]
		$weight = ($weight * 256) + $arr[27]

		$pri = $arr[24]
		$pri = ($pri * 256) + $arr[25]

		$nslen = $arr[30]
		$segments = $arr[31]
		$index = 32
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3} {4} {5}"
		}

		if ($age -eq 0)
		{
			$formatstring -f `
				$name, "[static]", `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}
		else
		{
			$formatstring -f `
				$name, ("[" + $timestamp.ToString() + "]"), `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}

	}
	else
	{
		$name
		$var.distinguishedname.ToString()
		dumpByteArray $arr
	}

}

function processAttribute([string]$attrName, [System.Object]$var)
{
	$array = $var.$attrName.Value
####	"{0} contains {1} rows of type {2} from {3}" -f $attrName, $array.Count, $array.GetType(), $var.distinguishedName.ToString()

	if ($array -is [System.Byte[]])
	{
####		dumpByteArray $array
####		" "
		analyzeArray $array $var
####		" "
	}
	else
	{
		for ($i = 0; $i -lt $array.Count; $i++)
		{
####			dumpByteArray $array[$i]
####			" "
			analyzeArray $array[$i] $var
####			" "
		}
	}
}

function usage
{
"
.\dns-dump -zone  [-dc ] [-csv] |
	   -help

dns-dump will dump, from Active Directory, a particular named zone. 
The zone named must be Active Directory integrated.

Zone contents can vary depending on domain controller (in regards
to replication and the serial number of the SOA record). By using
the -dc parameter, you can specify the desired DC to use. Otherwise,
dns-dump uses the default DC.

Usually, output is formatted for display on a workstation. If you
want CSV (comma-separated-value) output, specify the -csv parameter.
Use out-file in the pipeline to save the output to a file.

Finally, to produce this helpful output, you can specify the -help
parameter.

This command is basically equivalent to (but better than) the:

	dnscmd /zoneprint 
or
	dnscmd /enumrecords  '@'

commands.

Example 1:

	.\dns-dump -zone essential.local -dc win2008-dc-3

Example 2:

	.\dns-dump -help

Example 3:

	.\dns-dump -zone essential.local -csv |
            out-file essential.txt -encoding ascii

	Note: the '-encoding ascii' is important if you want to
	work with the file within the old cmd.exe shell. Otherwise,
	you can usually leave that off.
"
}

	##
	## Main
	##

	if ($help)
	{
		usage
		return
	}

	if ($args.Length -gt 0)
	{
		write-error "Invalid parameter specified"
		usage
		return
	}

	if (!$zone)
	{
		throw "must specify zone name"
		return
	}

	$root = [ADSI]"LDAP://RootDSE"
	$defaultNC = $root.defaultNamingContext

	$dn = "LDAP://"
	if ($dc) { $dn += $dc + "/" }
	$dn += "DC=" + $zone + ",CN=MicrosoftDNS,CN=System," + $defaultNC

	$obj = [ADSI]$dn
	if ($obj.name)
	{
		if ($csv)
		{
			"Name,Timestamp,TTL,RecordType,Param1,Param2"
		}

		#### dNSProperty has a different format than dNSRecord
		#### processAttribute "dNSProperty" $obj

		foreach ($record in $obj.psbase.Children)
		{
			####	if ($record.dNSProperty) { processAttribute "dNSProperty" $record }
			if ($record.dnsRecord)   { processAttribute "dNSRecord"   $record }
		}
	}
	else
	{
		write-error "Can't open $dn"
	}

	$obj = $null

Until next time...

If there are things you would like to see written about, please let me know!

Published Wednesday, June 17, 2009 2:09 PM by michael

Comments

Tuesday, December 22, 2009 7:23 PM by Michael's meanderings...

# Getting the Contents of an Active Directory Integrated DNS Zone, Version 2

In June of 2009, I published the first version of Getting the Contents of an Active Directory Integrated