Thursday, March 25, 2010

.NET and NetBIOS name resolution

Recently I was looking at what it would take to throw together some code to chase failed logon events through multiple servers and workstations down to the source machine, source process, and/or source network connection. One of the problems that I encounter in security event logs, the source machines are often IP addresses instead of the system name. When dealing with reverse DNS records in dynamic environments, there are problem subnets in which hosts change IP's often. Reverse DNS allows multiple hosts to record the same record with a maximum per entry limit so high that name resolution is worthless. Even if you have a short scavenging period, your data may not be very accurate, or perhaps the source machine did not register a record. One idea that came to mind was doing a WMI lookup against the remote IP to pull the computer name, but that requires the appropriate level of access for remote WMI (if it is enabled), so it may not be as reliable. Being a sysadmin from the pre-windows 2000 period, I like to use nbtstat against the IP address to see the name of remote windows machines. I tried looking around the various classes of .NET and could find anything for netbios style name resolution, so to save myself the trouble of trying to parse through nbtstat command output strings, and dealing with that slowness (when using multiple NIC's and virtualization NIC's), I decided to roll my own solution. The code below was expanded more later for some additional functionality such as flag parsing. This is the simplified version that returns an array of PSObjects containing the various netbios records. For those not familiar with Netbios, it is the 15 character name format used in windows. The 16th byte of the name is a type value. Type 0x00 will give you the workstation name and domain name (group flag is enabled). The netbios query packet is pretty standard other than perhaps the transaction ID. The replied records are all fixed length and names are padded with 0x20 up to the 15 characters for the names. There is a number of records value that tells how many records were returned, so pulling the results is pretty basic.   (NOTE: the script opens a privileged port, so it requires admin rights on the local machine that you are running it on)


Function convert-netbiosType([byte]$val) {
 #note netbios type codes are usually in decimal, but .net likes to deal with bytes
 #as integers.
    
 $myval = [int]$val
 switch($myval) {
  0 { return "Workstation" }
  1 { return "Messenger service" }
  3 { return "Messenger" }
  6 { return "RAS" }
  32 { return "File Service" }
  27 { return "Domain Master Browser" }
  28 { return "Domain Controller" }
  29 { return "Master Browser" }
  30 { return "Browser election" }
  31 { return "NetDDE" }
  33 { return "RAS Client" }
  34 { return "Exchange MS mail connector" }
  35 { return "Exchange Store" }
  36 { return "Exchange Directory" }
  48 { return "Modem sharing service Server"}
  49 { return "Modem sharing service Client"}
  67 { return "SMS client remote control" }
  68 { return "SMS client remote transfer" }
  135 { return "Exchange MTA" }
  default { return "unk" }
 }
 
}



function get-netbios-name ([string]$ip) {
 #Function:  Get netbios name of the remote machine by IP address provided
 #  result:  Error = $null, positive result is hashtable of names
 if (-not ($ip -match "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")) {
  write-error "The Ip address provided: $ip  is not a valid IPv4 address format"
  return $null
 }
 #ping first for reachability check
 $po = New-Object net.NetworkInformation.PingOptions
 $po.set_ttl(64)
 $po.set_dontfragment($true)
#alexander ping
 [Byte[]] $pingbytes = (65,72,79,89)
 $ping = new-object Net.NetworkInformation.Ping
 $pingres = $ping.send($ip, 1000, $pingbytes, $po)
    
if ($pingres.status -eq "Success") {
    
#netbios name query  NBTNS
 $port=137
 $ipEP = new-object System.Net.IPEndPoint ([system.net.IPAddress]::parse($ip),$port)
 $udpconn = new-Object System.Net.Sockets.UdpClient
 [byte[]] $sendbytes = (0xf4,0x53,00,00,00,01,00,00,00,00,00,00,0x20,0x43,0x4b,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41 ,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,00,00,0x21,00,01)
 $udpconn.client.receivetimeout=1000
 $bytesSent = $udpconn.Send($sendbytes,50,$ipEP)
 $rcvbytes = $udpconn.Receive([ref]$ipEP)
 if ($? -eq $false -or $rcvbytes.length -lt 63) {
  write-error "System is not responding to netbios traffic on port 137, system is not a windows machine, or other error has occurred."
  return $null
    
 } else {
  [array]$nbnames = $null
  #nbtns query results have a number of returned records field at byte #56 of the returned
  #udp payload.  Read this value to find how many records we have
  $startptr = 56
  $numresults = [int]$rcvbytes[$startptr]
  $startptr++
  $namereclen = 18
  #loop through the number of results and get the names + data
  #  NETBIOS result =  15 byte of name (padded if shorted 0x20)
  #                     1 byte of type
  #                     2 byte of flags
    
    
  for ($i = 0; $i -lt $numresults; $i++) {
   $nbname = new-object PSObject
   $tempname = ""
   #read the 15 byte name and convert to human readable string
   for ($j = 0; $j -lt $namereclen -3; $j++) {
    $tempname += [char]$rcvbytes[$startptr + ($i * $namereclen) + $j]
       
   }
   add-member -input $nbname NoteProperty NetbiosName $tempname
   $rectype = convert-netbiosType $rcvbytes[$startptr + ($i * $namereclen) + 15]
   add-member -input $nbname NoteProperty  RecordType $rectype
   if (($rcvbytes[$startptr + ($i * $namereclen) + 16] -band 128) -eq 128 ) {
  #in the flags field, only the high order byte of the 2 is used
  #the left most bit is the Group name flag which can be used for domain
  #name type identification to differentiate the 0x00 type names
    $groupflag = 1
   } else { $groupflag = 0 }
    add-member -input $nbname NoteProperty IsGroupType $groupflag
    $nbnames += $nbname
   }
   return $nbnames
    
  }
 } else {
  write-error "System not pinging: $ip"
  #prompt for another ip to be inputted?
  return $null
 }
  
}

$ip = args[0]
if ($ip -eq $null -or $ip -eq "") {
  write-host "You need to provide an ip address to check"
  exit
}
get-netbios-name $ip

For those that use nbtstat, you will also know that it returns the MAC address, however not in the best or most efficient way. Since nbtstat tries to use every network adapter on the machine, if you have multiple nics and virtualization nics, then it is slow. The benefit of the powershell method above is that you only send out from one NIC. To get the MAC address, the code can be modified to pull it out of the received packets. MAC address is the last useful 6 bytes of the packet (which may be padded with extra 0's at the end). So you can use this to grab those bytes and format it to a mac address string.

$mac = (0,0,0,0,0,0)
$j = 5
for ($i = $rcvbytes.length - 1; $i -gt 0; $i--) {
   if ($rcvbytes[$i] -ne 0x0) {
      $mac[$j] = $rcvbytes[$i]
      $j--
      if ($j -eq -1) { $i = -1 }
   }
}
$macstring = ""
foreach ($byte in $mac) {
  $macstring += ("{0:X2}" -f $byte) + "-"
}
new-object psobject -property @{
   IP = $ip
   MacAddress = $macstring.trim("-")
}

3 comments:

  1. Excellent code. Heads up there's an error in getting the mac address.

    foreach (byte in $mac) {
    should read:
    foreach ($byte in $mac) {

    Thanks heaps for sharing!!!

    ReplyDelete
  2. another small little bug in the get mac address.
    MacAddress = $macstring.trim("=")
    should read:
    MacAddress = $macstring.trim("-")

    ReplyDelete
  3. Thanks for catching that Viriio, I have updated it with the corrections.

    ReplyDelete