10

While reading this doc https://wiki.wireshark.org/CaptureFilters I found this line:

tcp[((tcp[12:1] & 0xf0) >> 2):4]

which figures out the TCP Header Length, but I can't find out how it really works (in detail).

Can somebody explain it?

Neymour
  • 103
  • 1
  • 5

1 Answers1

24

I presume you pulled this from the "capture HTTP GET requests" filter example.

First, some notation:

  • var[n] means the nth byte of var.
  • var[n,c] means c bytes of var starting at offset n, e.g. var[3:4] would return bytes 3,4,5,6 of var.
  • & is the bitwise AND operation.
  • >> means bitwise shift right.

So, what do we have?

tcp[((tcp[12:1] & 0xf0) >> 2):4]

Let's deconstruct this into its individual parts:

tcp[12:1] takes 1 byte of the TCP segment (i.e. the packet including header) at offset 12. We can see from the structure that offset 12 (0xC) is the Data Offset field. Its definition is as follows:

Data offset (4 bits) specifies the size of the TCP header in 32-bit words. The minimum size header is 5 words and the maximum is 15 words thus giving the minimum size of 20 bytes and maximum of 60 bytes, allowing for up to 40 bytes of options in the header. This field gets its name from the fact that it is also the offset from the start of the TCP segment to the actual data.

Ok, cool. This field is only 4 bits, though, and tcp[12:1] takes a whole byte (8 bits). We only want the first four.

tcp[12:1] & 0xf0 takes that Data Offset byte and applies a bitwise AND operation, using the constant 0xF0. This is often known as a masking operation, since any bits in the constant set to 0 will also be set to zero in the output. It's easier to think about this in binary. 0xF0 is just 11110000, and since x & 0 for any bit x is always 0, and x & 1 for any bit x is always x, it follows that the zero bits in 0xF0 will "switch off" or mask the given bits, but leave the rest alone. In this case, if we imagine that tcp[12:1] is 10110101, the result would be 10110000 because the last four bits are masked to zero. The idea here is that, since the Data Offset field is only 4 bits long, but we have 8 bits, we mask out the irrelevant bits so we only have the first 4.

The problem here is that, numerically speaking, our 4 bits are in the "top" side of the byte. This means that instead of just having 1101 (our Data Offset bits), we have 11010000. If we just wanted to get the "raw" value of the Data Offset field, we'd right shift by four places. 10110000 >> 4 = 1101, i.e. we throw away the "bottom" 4 bits and shift the top four bits right. However, in this case you'll notice the filter only shifts across by 2 places, not 4.

This is where we want to refer back to the definition of the Data Offset field: it specifies the size of the header in 32-bit words, not bytes. So, if we want to know the length of the header in bytes, we need to multiply it by 4. As it turns out, a bitwise left-shift of 1 is the same as multiplying a number by 2, and a bitwise left-shift of 2 is the same as multiplying a number by 4.

Now, combine this with what we've already seen: >> 4 would make sense in the filter if you wanted to get that raw value of Data Offset, but then we want to multiply it by 4, which is equivilent to left shifting (<< 2), which cancels out part of that right shift, resulting in >> 2.

So, (tcp[12:1] & 0xf0) >> 2 extracts the Data Offset field and multiplies it by 4 to get us the size of the TCP header in bytes.

But wait, there's more!. In the filter you provided, we still need to do one more operation:

tcp[((tcp[12:1] & 0xf0) >> 2):4]

This is easier looked at if we used an intermediate variable:

$offset = ((tcp[12:1] & 0xf0) >> 2)
tcp[$offset:4]

What this does is get the first 4 bytes after the TCP header, i.e. the first 4 data bytes of the payload. In the filter you pulled this from, they were looking for HTTP GET requests using this filter:

port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420

The 0x47455420 constant is actually a numeric encoding of the ASCII bytes for GET (that last character is a space), where the ASCII values of those characters are 0x47, 0x45, 0x54, 0x20.

So, how does this work in full? It extracts the 4-bit Data Offset field from the TCP header, multiplies it by 4 to compute the size of the header in bytes (which is also the offset to the data), then extracts 4 bytes at this offset to get the first 4 bytes of the data, which it then compares to "GET " to check it's a HTTP GET.

Polynomial
  • 132,208
  • 43
  • 298
  • 379
  • 1
    Love the answer and hence awarding bounty on this one. In case anyone is curious why shifting is necessary (and I think this could be clarified in the answer a bit more), this is because Data Offset value treats packet in 32 bit chunks, but TCP dump looks at bytes. We know that there's 4 bytes in 32 bit chunk. Hence, after we get raw value of 32 bit chunks by shifting `>> 4` we need to multiply by 4, which is binary shift `<< 2`. So instead of shifting right 4, then left 2, we can just do one shift right by two places aka `>> 2` – Sergiy Kolodyazhnyy Jan 01 '20 at 23:15
  • @SergiyKolodyazhnyy Thanks for the bounty! The shifting by 2 is actually mentioned in the answer already, do a Ctrl+F for "Now, combine this with" for the relevant paragraph. – Polynomial Jan 01 '20 at 23:17
  • Yes, I saw that part, I just thought it could be explained in more detail. Maybe it's just me. But regardless of the fact, I love this answer – Sergiy Kolodyazhnyy Jan 01 '20 at 23:23