BPF Filters Across Protocol Header Boundaries

BPF has keywords to address layer 3 and layer 4 protocols, such as IP, TCP, UDP and ICMP. But what if you need check the value of a field in a higher layer protocol, such as DNS? If the header you're working with is static, that is, never changes size like UDP, you can then go past the end of that header and into the next. Johnathan Ham teaches this in the excellent FOR558 Network Forensics class at SANS.
To use his example, say you wanted to filter packets to see only DNS queries. The QR bit in the DNS header, which specifies whether a DNS packet is a query or an answer, is found in the 2nd byte offset from zero in the DNS header, or the third byte. Since a UDP header is always exactly eight bytes long, we can extend what byte we specify past the end of that header and into the DNS header. The last UDP byte is number 7 (it consists of bytes 0-7), and we want to drill down to the second byte from zero in the DNS header. Counting from that 7th byte, that would be the 10th byte from the beginning of UDP. Our field is one byte in length, and the QR bit is first bit on the left, or as it's known, the highest order (or most significant) bit. Networking is big endian, meaning we address bits from the left to the right, the same way English speakers read. Now we need to apply our bit masking to check the value of just that one bit. As we know, we want to apply a bitwise AND operation to all the bits, using a one to preserve the bit and a zero to mask the bits we don't care about. So our byte is laid out like this:

QR  Opcode   AA TC RD
 0     1  2 3 |4   5    6    7

Our first nibble, or four bits, contains the QR bit and the first three bits of the Opcode field (bits 0-3).
Our second nibble contains the last Opcode bit, the AA bit, the TC bit and the RD bit (bits 4-7).

If you want to know what those other bits are used for, you can take a look here.

So our mask would be 1000 0000 in binary. Our first nibble has a one in the 8's column (2 to the power of 3) and everything else is zeroed out, so in hex our mask would be 0x80. We apply that mask using our BPF filter and look for a value of 0 in that bit (which designates a query, as a value of 1 designates a response).

We can add port 53 for some extra assurance we get DNS queries and not syslog or other UDP traffic. The tcpdump command I used is: tcpdump -nnvvv -i eth0 'udp[10] & 0x80 = 0 and port 53'

And sure enough, what I end up is packets that look something like this:


21:20:49.596637 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 60)
    x.x.x.x.2051 > 208.67.222.123.53: [udp sum ok] 2800+ A? ws.immunet.com. (32)

Here we see a query from one of my boxes (munged to x.x.x.x) to an OpenDNS server for the address record of ws.immunet.com.

Now if we change the value from 0 to our mask value in our filter (udp[10] & 0x80 = 0x80), we can look for DNS answers instead. (Or we could just specify not 0, since the only other possible value would be a one). So we could use 'udp[10] & 0x80 != 0'
Either one would work. Very nice!