This is the second post of a short series of entries that demonstrate the creation of a simple security/penetration-testing application. The end-result will be a simple NetBIOS Name Service spoofer, written in Python.
If you’re enjoying the packet analysis aspects of this, you might be interested in the SANS IP Packet Analysis course I’ll be teaching soon
In the previous part of this series, we took a look at the NetBIOS Name Service (NBNS) query and response packets in order to get an idea of what we would need to do to craft our spoofed responses. Today, we’re actually going to start writing some code, with the goal being to get a basic skeleton of our spoofer up and running. At the end of this part, we’ll have nbnspoof to the point that it’ll sniff for, dissect, and display NBNS queries and responses,
If you’re following along at home, the only requirements for the code is a working installation of Python and the excellent packet manipulation library Scapy. Scapy may be in your operating system’s repositories (it is for Ubuntu at least). If it’s not, it shouldn’t be too difficult to install by hand, following the instructions on the Scapy homepage. The code we’re looking at today is available at:
One thing that we want to do before we start coding is to see how Scapy decodes these NBNS packets. Scapy has an interactive mode that gives you access to a python interpreter and all of the scapy functionality, so we can use it to look at one of our packet dumps from yesterday:
weasel@hacktop:~/Desktop/nbnspoof$ scapy Welcome to Scapy (126.96.36.199beta) >>> pkts = rdpcap("ping_with_nbns_response.pcap") >>> pkts <ping_with_nbns_response.pcap: ICMP:8 UDP:6 TCP:0 Other:4> >>> pkts.show() 0000 Ether / ARP who has 172.16.185.2 says 172.16.185.133 0001 Ether / ARP is at 00:50:56:e8:0b:97 says 172.16.185.2 0002 Ether / IP / UDP / DNS Qry "xpprotest2.localdomain." 0003 Ether / IP / UDP / DNS Ans 0004 Ether / IP / UDP / DNS Qry "xpprotest2.localdomain." 0005 Ether / IP / UDP / DNS Ans 0006 Ether / IP / UDP 172.16.185.133:netbios-ns > 172.16.185.255:netbios-ns / NBNSQueryRequest 0007 Ether / ARP who has 172.16.185.133 says 172.16.185.132 0008 Ether / ARP is at 00:0c:29:27:b9:f0 says 172.16.185.133 0009 Ether / IP / UDP 172.16.185.132:netbios-ns > 172.16.185.133:netbios-ns / NBNSQueryRequest / Raw 0010 Ether / IP / ICMP 172.16.185.133 > 172.16.185.132 echo-request 0 / Raw 0011 Ether / IP / ICMP 172.16.185.132 > 172.16.185.133 echo-reply 0 / Raw 0012 Ether / IP / ICMP 172.16.185.133 > 172.16.185.132 echo-request 0 / Raw 0013 Ether / IP / ICMP 172.16.185.132 > 172.16.185.133 echo-reply 0 / Raw 0014 Ether / IP / ICMP 172.16.185.133 > 172.16.185.132 echo-request 0 / Raw 0015 Ether / IP / ICMP 172.16.185.132 > 172.16.185.133 echo-reply 0 / Raw 0016 Ether / IP / ICMP 172.16.185.133 > 172.16.185.132 echo-request 0 / Raw 0017 Ether / IP / ICMP 172.16.185.132 > 172.16.185.133 echo-reply 0 / Raw
In the above session, I started Scapy, loaded a list of packets from a pcap dump, showed a summary of the number of packets of each type, and then listed a summary of each packet in the dump. From the looks of the output, it seems that packet 6 is the NBNS query, and packet 9 is the result. Let’s take a close look at those:
>>> pkts <Ether dst=ff:ff:ff:ff:ff:ff src=00:0c:29:27:b9:f0 type=IPv4 | <IP version=4L ihl=5L tos=0x0 len=78 id=64 flags= frag=0L ttl=128 proto=UDP chksum=0x6eb9 src=172.16.185.133 dst=172.16.185.255 options='' |<UDP sport=netbios-ns dport=netbios-ns len=58 chksum=0x5224 |<NBNSQueryRequest NAME_TRN_ID=32799 FLAGS=272 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 QUESTION_NAME='XPPROTEST2 ' SUFFIX=workstation NULL=0 QUESTION_TYPE=NB QUESTION_CLASS=INTERNET |>>>> >>> pkts <Ether dst=00:0c:29:27:b9:f0 src=00:0c:29:03:ad:f7 type=IPv4 | <IP version=4L ihl=5L tos=0x0 len=90 id=35 flags= frag=0L ttl=128 proto=UDP chksum=0x6f45 src=172.16.185.132 dst=172.16.185.133 options='' |<UDP sport=netbios-ns dport=netbios-ns len=70 chksum=0xd516 |<NBNSQueryRequest NAME_TRN_ID=32799 FLAGS=34048 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 QUESTION_NAME='XPPROTEST2 ' SUFFIX=workstation NULL=0 QUESTION_TYPE=NB QUESTION_CLASS=INTERNET | <Raw load='\x00\x04\x93\xe0\x00\x06\x00\x00\xac\x10\xb9\x84' |>>>>>
From the above, Scapy appears to recognize and dissect the packets (which are the same packet we looked at in Part 1 in Wireshark) fairly well. It looks like we’ll have to mask out bits in the “FLAG” field ourselves, but that’s not a big deal. Also, you’ll notice that the response is the same thing as a query basically, with the actual “answer”, including the IP address, tacked on at the end in the “Raw load” layer. This means that when we build our responses up in our code, we’ll have to handle all the fields in this section ourselves, which won’t be hard. We can use Wireshark as a reference to see how it is dissected/crafted.
One nice thing we can observe from what Scapy has done with these packets is that it has the ability to decode and encode the names from that crazy encoding scheme we saw in the previous post. That’ll save us some headaches and effort. This encoding is called “First Level Encoding”, and was created in some sort of attempt at getting NetBIOS to play nice with DNS. It involves taking each byte of the name, splitting apart the upper and lower 4 bits, and adding each 4 bits to the letter ‘A’ in hex. It’s not too complex, but it’s nice that we won’t have to deal with it in our code .
Speaking of code, we’re at a place that we can start writing some. For larger projects you’ll want some sort of requirements and/or design documents to help guide your process, but this is going to be a very simple program. Even in its simplicity, however, you want some sort of guideline for how you want your program to operate. In this case, I want the ability to tie nbnspoof to an interface, have it listen for any NBNS queries for names matching a regular expression, and craft responses to these queries that points them to a given IP address.
Given this information, I want to write the “usage” text for the program first, so I have some reminder of how it should behave. This will be what is displayed if someone runs nbnspoof with zero or invalid arguments. This may change in the development of the program, but here’s what we’re starting with:
def usage(): print """Usage: nbnspoof.py [-v] -i <interface> -n <regexp> -h <ip address> -v Verbose output of sniffed NBNS name queries, and responses sent -i The interface you want to sniff and send on -n A regular expression applied to each query to determine whether a spoofed response will be sent -h The IP address that will be sent in spoofed responses """ return
(I’ll be skipping around in the code, so if you’re hardcore into this or have questions, you may want to follow along in the source code itself to see what I’m skipping)
So we have three required arguments (they’re in angle braces): an interface to listen and inject on, a regular expression for names to match, and an IP address to send in the responses. There is a single optional argument, in square brackets, that specifies whether or not we want “verbose” output. The verbose output will include a summary of NBNS requests and queries as they are sniffed, as well as notification of what packets it has crafted and sent off.
To parse these arguments taken in from the command line, we use Python’s “getopt” module in the following code:
def main(): global verbose try: opts, args = getopt.getopt(sys.argv[1:],"vi:n:h:") except: usage() sys.exit(1) verbose = False interface = None name_regexp = None ip = None for o, a in opts: if o == '-v': verbose = True if o == '-i': interface = a if o == '-n': name_regexp = a if o == '-h': ip = a if args or not ip or not name_regexp or not interface: usage() sys.exit(1)
It’s fairly simple. The list of arguments passed from the shell is given to getopts, with a format string to tell it what options to look for, and which ones have arguments (designated by a colon after the letter). If this throws an exception (usually because the user didn’t supply any arguments), the usage text is displayed and the program exits.
After declaring our variables with default values, we go into an interesting loop. “opts” holds an array of tuples, each one containing an option and its argument. For each, we test to see if it’s one we know and care about, and set up our variables with that option’s argument. After we’re finished doing that, we check to see if any of our required options were not given, and whether or not we have any extra arguments left over. If anything seems fishy, we remind the user of the usage() and exit.
All of this required a bit of effort, and makes up what will be a good chunk of our code, but it helps the program look professional. A penetration tester of any skill should be able to pick it up and figure out how to use this in a fairly short amount of time.
With the preliminaries and preparation out of the way, we can get down to some serious network business! You really won’t believe how easy it is to set up a sniffer using the Scapy library. Here we go:
sniff(iface=interface,filter="udp and port 137",store=0,prn=get_packet)
Wow, huh? It’s pretty self explanatory, but here we go: First, we tell it what interface to sniff and inject on, based on what the user told us (eth0, eth1, etc). Next, we have a BPF filter that tells the libpcap library to only send us UDP packets that involve port 137. This is for the sake of performance, and to prevent cases where Scapy might accidentally identify something on another port as being NBNS traffic (Perhaps someone trying to detect a user of nbnspoof would craft NBNS packets on another port to see if the nbnspoof user would respond when a Windows machine wouldn’t). The “store” argument is set to zero, because once we’ve dealt with each packet, we’re going to throw it away. Otherwise, the sniff function will store and return a list of packets, which would waste memory, as we won’t be using it.
Finally, the “prn” argument is set to get_packet. “prn” allows you to set a “call-back” function. What this means, is that for every packet that sniff() sees, it will pass that packet to the call-back function. Here, we have set it to get_packet, which is our function for dissecting, displaying, and crafting packets based off the NBNS queries we see. This function is where most of the real work of nbnspoof will be done. Let’s take a look at it is working so far:
def get_packet(pkt): if not pkt.getlayer(NBNSQueryRequest): return if pkt.FLAGS & 0x8000: query = False ip = '127.0.0.1' else: query = True
First off, we see if the packet has the “NBNSQueryRequest” layer, as far as Scapy is concerned. This will help us weed out anything that might show up on this port that isn’t NBNS related. Remember, that Scapy sees NBNS response packets the same way, so both queries and results will pass this test.
Next, we test the FLAGS section of the NBNS data to see if this packet is a query or request, we do this by testing to see if the flags, logically AND’d with 0×8000 (binary: 1000000000000000) is true or not. If the bit is set, then it is a response. Now, for the sake of the “verbose” option, we would want to decode from the packet what IP address this response would be. Right now, we don’t have this code in place, so we’re just putting in ’127.0.0.1′ as a placeholder. If the bit isn’t set, then it’s a query.
if verbose: print str(pkt.NAME_TRN_ID) + ":", if query: print "Q", else: print "R", print "SRC:" + pkt.getlayer(IP).src + " DST:" + pkt.getlayer(IP).dst, if query: print 'NAME:"' + pkt.QUESTION_NAME + '"' else: print 'NAME:"' + pkt.QUESTION_NAME + '"', print 'IP:' + ip
If we have the “verbose” option set, we want to display a summary of the current packet. This includes the transaction ID that uniquely pairs a question and response, the status of it being a query or response, source and destination IPs, and what name is being looked up. Now that we’ve covered what we have of the code so far, let’s use it to watch NBNS traffic between two Windows VMs:
weasel@hacktop:~/Desktop/nbnspoof$ sudo ./nbnspoof.py -v -i vmnet8 -n unused -h unused 32878: Q SRC:172.16.185.133 DST:172.16.185.255 NAME:"XPPROTEST2 " 32878: R SRC:172.16.185.132 DST:172.16.185.133 NAME:"XPPROTEST2 " IP:127.0.0.1 32879: Q SRC:172.16.185.133 DST:172.16.185.255 NAME:"EXAMPLE.CPM " 32879: Q SRC:172.16.185.133 DST:172.16.185.255 NAME:"EXAMPLE.CPM " 32879: Q SRC:172.16.185.133 DST:172.16.185.255 NAME:"EXAMPLE.CPM "
You’ll notice that even though we’re not doing anything with the regexp or IP address, we still need to specify them to get past our own checks . The above output is from pinging from “xpprotest” to “xpprotest2″, and then attempting to ping “example.cpm”. Some things to note:
- Transaction IDs for NBNS seem to be sequential! Stick that in your pocket for the next time you’re doing passive profiling/fingerprinting. Maybe I should try sniffing a machine as it boots up to see if it always starts at the same number.
- The name is always going to be 15 characters, all caps, padded out with spaces. This has to do with the encoding of the name in these NBNS packets, rather than a limitation of our script or Scapy. If a host tries to resolve a non-existent name that’s longer than this, it doesn’t try NBNS, so this’ll limit what names we can spoof for.
I hope you enjoyed this! The next part will take us through matching names we want to spoof for, dissecting and crafting response packets, and sending them to the machines that broadcast the queries.