DHCP Discovery

A PowerShell script to send a DHCP Discover request and listen for DHCP Offer responses, it can be used for finding DHCP servers (including rogue servers), or for testing DHCP servers and relays. The output from this script is an object containing a decode of the DHCP packet and a number of options.

By default the script uses a spoofed MAC address of AA:BB:CC:DD:EE:FF to send the offer, a specific MAC address can be specified using the MacAddressString parameter.

The script can listen for DHCP offers for a specified number of seconds by using the DiscoverTimeout parameter, by default the script listens for 60 seconds. Note that a timeout is set for the ReceiveFrom method, this is set to 10 seconds in the script and applies to each receive attempt. If the connection times out the script will stop listening for offers regardless of the value for DiscoverTimeout.

This script has only been tested using PowerShell 2.0.

<#
  Net-DhcpDiscover.ps1
  
  Author: Chris Dent
  Date: 16/02/2010
  
  A script to send a DHCPDISCOVER request and report on DHCPOFFER 
  responses returned by all DHCP Servers on the current subnet.
  
  DHCP Packet Format (RFC 2131 - http://www.ietf.org/rfc/rfc2131.txt):

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     op (1)    |   htype (1)   |   hlen (1)    |   hops (1)    |
    +---------------+---------------+---------------+---------------+
    |                            xid (4)                            |
    +-------------------------------+-------------------------------+
    |           secs (2)            |           flags (2)           |
    +-------------------------------+-------------------------------+
    |                          ciaddr  (4)                          |
    +---------------------------------------------------------------+
    |                          yiaddr  (4)                          |
    +---------------------------------------------------------------+
    |                          siaddr  (4)                          |
    +---------------------------------------------------------------+
    |                          giaddr  (4)                          |
    +---------------------------------------------------------------+
    |                                                               |
    |                          chaddr  (16)                         |
    |                                                               |
    |                                                               |
    +---------------------------------------------------------------+
    |                                                               |
    |                          sname   (64)                         |
    +---------------------------------------------------------------+
    |                                                               |
    |                          file    (128)                        |
    +---------------------------------------------------------------+
    |                                                               |
    |                          options (variable)                   |
    +---------------------------------------------------------------+

    FIELD      OCTETS       DESCRIPTION
    -----      ------       -----------

    op            1  Message op code / message type.
                     1 = BOOTREQUEST, 2 = BOOTREPLY
    htype         1  Hardware address type, see ARP section in "Assigned
                     Numbers" RFC; e.g., '1' = 10mb ethernet.
    hlen          1  Hardware address length (e.g.  '6' for 10mb
                     ethernet).
    hops          1  Client sets to zero, optionally used by relay agents
                     when booting via a relay agent.
    xid           4  Transaction ID, a random number chosen by the
                     client, used by the client and server to associate
                     messages and responses between a client and a
                     server.
    secs          2  Filled in by client, seconds elapsed since client
                     began address acquisition or renewal process.
    flags         2  Flags (see figure 2).
    ciaddr        4  Client IP address; only filled in if client is in
                     BOUND, RENEW or REBINDING state and can respond
                     to ARP requests.
    yiaddr        4  'your' (client) IP address.
    siaddr        4  IP address of next server to use in bootstrap;
                     returned in DHCPOFFER, DHCPACK by server.
    giaddr        4  Relay agent IP address, used in booting via a
                     relay agent.
    chaddr       16  Client hardware address.
    sname        64  Optional server host name, null terminated string.
    file        128  Boot file name, null terminated string; "generic"
                     name or null in DHCPDISCOVER, fully qualified
                     directory-path name in DHCPOFFER.
    options     var  Optional parameters field.  See the options
                     documents for a list of defined options.
#>

#
# Parameters
#

param (
    # MAC Address String in Hex-Decimal Format can be delimited with 
    # dot, dash or colon (or none)
    [String]$MacAddressString = "AA:BB:CC:DD:EE:FF",

    # Length of time (in seconds) to spend waiting for Offers if
    # the connection does not timeout first
    [Byte]$DiscoverTimeout = 60
)

# Build a DHCPDISCOVER packet to send
#
# Caller: Main

function New-DhcpDiscoverPacket {
    param (
        [String]$MacAddressString = "AA:BB:CC:DD:EE:FF"
    )

    # Generate a Transaction ID for this request

    $XID = New-Object Byte[] 4
    $Random = New-Object Random
    $Random.NextBytes($XID)

    # Convert the MAC Address String into a Byte Array

    # Drop any characters which might be used to delimit the string
    $MacAddressString = $MacAddressString -replace "-|:|\."
    $MacAddress = [BitConverter]::GetBytes(
        [UInt64]::Parse(
            $MacAddressString, 
            [Globalization.NumberStyles]::HexNumber
        )
    )
    # Reverse the MAC Address array
    [Array]::Reverse($MacAddress)

    # Create the Byte Array
    $DhcpDiscover = New-Object Byte[] 243

    # Copy the Transaction ID Bytes into the array
    [Array]::Copy($XID, 0, $DhcpDiscover, 4, 4)

    # Copy the MacAddress Bytes into the array (drop the first 2 bytes, 
    # too many bytes returned from UInt64)
    [Array]::Copy($MACAddress, 2, $DhcpDiscover, 28, 6)

    # Set the OP Code to BOOTREQUEST
    $DhcpDiscover[0] = 1
    # Set the Hardware Address Type to Ethernet
    $DhcpDiscover[1] = 1
    # Set the Hardware Address Length (number of bytes)
    $DhcpDiscover[2] = 6
    # Set the Broadcast Flag
    $DhcpDiscover[10] = 128
    # Set the Magic Cookie values
    $DhcpDiscover[236] = 99
    $DhcpDiscover[237] = 130
    $DhcpDiscover[238] = 83
    $DhcpDiscover[239] = 99
    # Set the DHCPDiscover Message Type Option
    $DhcpDiscover[240] = 53
    $DhcpDiscover[241] = 1
    $DhcpDiscover[242] = 1

    $DhcpDiscover
}

# Parse a DHCP Packet, returning an object containing each field
# 
# Caller: Main

function Read-DhcpPacket {
    param (
        [Byte[]]$Packet
    )

    $Reader = New-Object IO.BinaryReader(New-Object IO.MemoryStream(@(,$Packet)))
    $DhcpResponse = New-Object PSObject

    # Get and translate the Op code
    $DhcpResponse | Add-Member Op $Reader.ReadByte()
    if ($DhcpResponse.Op -eq 1) { 
        $DhcpResponse.Op = "BootRequest" 
    } else { 
        $DhcpResponse.Op = "BootResponse" 
    }

    $DhcpResponse | Add-Member HType -Value $Reader.ReadByte()
    if ($DhcpResponse.HType -eq 1) {
        $DhcpResponse.HType = "Ethernet"
    }

    $DhcpResponse | Add-Member HLen $Reader.ReadByte()
    $DhcpResponse | Add-Member Hops $Reader.ReadByte()
    $DhcpResponse | Add-Member XID $Reader.ReadUInt32()
    $DhcpResponse | Add-Member Secs $Reader.ReadUInt16()
    $DhcpResponse | Add-Member Flags $Reader.ReadUInt16()

    # Broadcast is the only flag that can be present, the other bits are reserved
    if ($DhcpResponse.Flags -band 128) { 
        $DhcpResponse.Flags = "Broadcast"
    }

    $DhcpResponse | Add-Member CIAddr ('{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4))
    $DhcpResponse | Add-Member YIAddr ('{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4))
    $DhcpResponse | Add-Member SIAddr ('{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4))
    $DhcpResponse | Add-Member GIAddr ('{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4))

    $MacAddrBytes = New-Object Byte[] 16
    $null = $Reader.Read($MacAddrBytes, 0, 16)
    $MacAddress = ($MacAddrBytes[0..5] | ForEach-Object { '{0:X2}' -f $_ }) -join ':'
    $DhcpResponse | Add-Member NoteProperty CHAddr $MacAddress

    $DhcpResponse | Add-Member SName ($Reader.ReadChars(64) -join '').Trim()
    $DhcpResponse | Add-Member File ($Reader.ReadChars(128) -join '').Trim()

    $DhcpResponse | Add-Member MagicCookie ('{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4))

    # Start reading Options

    $DhcpResponse | Add-Member NoteProperty Options @()
    while ($Reader.BaseStream.Position -lt $Reader.BaseStream.Length) {
        $Option = New-Object PSObject
        $Option | Add-Member OptionCode $Reader.ReadByte()
        $Option | Add-Member OptionName ""
        $Option | Add-Member Length 0
        $Option | Add-Member OptionValue ""

        if ($Option.OptionCode -ne 0 -And $Option.OptionCode -ne 255) {
            $Option.Length = $Reader.ReadByte()
        }

        switch ($Option.OptionCode) {
            0 {
                $Option.OptionName = "PadOption"
            }
            1 {
                $Option.OptionName = "SubnetMask"
                $Option.OptionValue = '{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4)
            }
            3 {
                $Option.OptionName = "Router"
                $Option.OptionValue = '{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4)
            }
            6 {
                $Option.OptionName = "DomainNameServer"
                $Option.OptionValue = @()
                for ($i = 0; $i -lt ($Option.Length / 4); $i++) { 
                    $Option.OptionValue += '{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4)
                }
            }
            15 {
                $Option.OptionName = "DomainName"
                $Option.OptionValue = $Reader.ReadChars($Option.Length) -join ''
            }
            51 {
                $Option.OptionName = "IPAddressLeaseTime"
                # Read as Big Endian
                $Value = ($Reader.ReadByte() * [Math]::Pow(256, 3)) +
                    ($Reader.ReadByte() * [Math]::Pow(256, 2)) +
                    ($Reader.ReadByte() * 256) +
                    $Reader.ReadByte()
                $Option.OptionValue = New-TimeSpan -Seconds $Value
            }
            53 { 
                $Option.OptionName = "DhcpMessageType"
                $Option.OptionValue = switch ($Reader.ReadByte()) {
                    1 { "DHCPDISCOVER" }
                    2 { "DHCPOFFER" }
                    3 { "DHCPREQUEST" }
                    4 { "DHCPDECLINE" }
                    5 { "DHCPACK" }
                    6 { "DHCPNAK" }
                    7 { "DHCPRELEASE" }
                }
            }
            54 {
                $Option.OptionName = "DhcpServerIdentifier"
                $Option.OptionValue = '{0}.{1}.{2}.{3}' -f $Reader.ReadBytes(4)
            }
            58 {
                $Option.OptionName = "RenewalTime"
                # Read as Big Endian
                $Value = ($Reader.ReadByte() * [Math]::Pow(256, 3)) +
                    ($Reader.ReadByte() * [Math]::Pow(256, 2)) +
                    ($Reader.ReadByte() * 256) +
                    $Reader.ReadByte()
                $Option.OptionValue = $(New-TimeSpan -Seconds $Value)
            }
            59 {
                $Option.OptionName = "RebindingTime"
                # Read as Big Endian
                $Value = ($Reader.ReadByte() * [Math]::Pow(256, 3)) +
                    ($Reader.ReadByte() * [Math]::Pow(256, 2)) +
                    ($Reader.ReadByte() * 256) +
                    $Reader.ReadByte()
                $Option.OptionValue = $(New-TimeSpan -Seconds $Value)
            }
            255 {
                $Option.OptionName = "EndOption"
            }
            default {
                # For all options which are not decoded here
                $Option.OptionName = "NoOptionDecode"
                $Buffer = New-Object Byte[] $Option.Length
                $null = $Reader.Read($Buffer, 0, $Option.Length)
                $Option.OptionValue = $Buffer
            }
        }
  
        # Override the ToString method
        $Option | Add-Member ToString -MemberType ScriptMethod -Force -Value {
            return "$($this.OptionName) ($($this.OptionValue))"
        }

        $DhcpResponse.Options += $Option
    }
    
    return $DhcpResponse
}

# Create a UDP Socket with Broadcast and Address Re-use enabled.
#
# Caller: Main

function New-UdpSocket {
    param (
        [Int32]$SendTimeOut = 5,
        [Int32]$ReceiveTimeOut = 5
    )
  
    $UdpSocket = New-Object Net.Sockets.Socket(
        [Net.Sockets.AddressFamily]::InterNetwork, 
        [Net.Sockets.SocketType]::Dgram,
        [Net.Sockets.ProtocolType]::Udp
    )
    $UdpSocket.EnableBroadcast = $true
    $UdpSocket.ExclusiveAddressUse = $false
    $UdpSocket.SendTimeOut = $SendTimeOut * 1000
    $UdpSocket.ReceiveTimeOut = $ReceiveTimeOut * 1000
    
    $UdpSocket
}

# Close down a Socket
#
# Caller: Main

function Remove-Socket {
    param (
        [Net.Sockets.Socket]$Socket
    )
    
    $Socket.Shutdown("Both")
    $Socket.Close()
}

#
# Main
#

# Create a Byte Array for the DHCPDISCOVER packet
$Message = New-DhcpDiscoverPacket -Send 10 -Receive 10

# Create a socket
$UdpSocket = New-UdpSocket

# UDP Port 68 (Server-to-Client port)
$EndPoint = [Net.EndPoint](New-Object Net.IPEndPoint([IPAddress]::Any, 68))

# Listen on $EndPoint
$UdpSocket.Bind($EndPoint)

# UDP Port 67 (Client-to-Server port)
$EndPoint = [Net.EndPoint](New-Object Net.IPEndPoint([IPAddress]::Broadcast, 67))
# Send the DHCPDISCOVER packet
$BytesSent = $UdpSocket.SendTo($Message, $EndPoint)

# Begin receiving and processing responses
$NoConnectionTimeOut = $True

$Start = Get-Date

while ($NoConnectionTimeOut) {
    $BytesReceived = 0
    try {
        # Placeholder EndPoint for the Sender
        $SenderEndPoint = [Net.EndPoint](New-Object Net.IPEndPoint([IPAddress]::Any, 0))
        # Receive Buffer
        $ReceiveBuffer = New-Object Byte[] 1024
        $BytesReceived = $UdpSocket.ReceiveFrom($ReceiveBuffer, [Ref]$SenderEndPoint)
      
        if ($BytesReceived -gt 0) {
            Read-DhcpPacket $ReceiveBuffer[0..$BytesReceived]
        }
      
        if ((Get-Date) -gt $Start.AddSeconds($DiscoverTimeout)) {
            # Exit condition, not error condition
            $NoConnectionTimeOut = $False
        }

    } catch [Net.Sockets.SocketException] {
        #
        # Catch a SocketException, thrown when the Receive TimeOut value is reached
        #
        $NoConnectionTimeOut = $false
    } catch {
        throw
    } 
}

Remove-Socket $UdpSocket
DHCP Discovery
Share this