Decoding The ICMP Packets With Python | For Hackers

What Is ICMP

The Internet Control Message Protocol (ICMP) is a supporting protocol in the Internet protocol suite. It is used by network devices, including routers, to send error messages and operational information indicating success or failure when communicating with another IP address, for example, an error is indicated when a requested service is not available or that a host or router could not be reached.ICMP differs from transport protocols such as TCP and UDP in that it is not typically used to exchange data between systems, nor is it regularly employed by end-user network applications (with the exception of some diagnostic tools like ping and traceroute).

Decoding The ICMP Packets

Now that we can fully decode the IP layer of any sniffed packets, we have to be able to decode the ICMP responses that our scanner will elicit from sending UDP datagrams to closed ports. ICMP messages can vary greatly in their contents, but each message contains three elements that stay consis- tent: the type, code, and checksum fields. The type and code fields tell the receiving host what type of ICMP message is arriving, which then dictates how to decode it properly.

For the purpose of our scanner, we are looking for a type value of 3 and a code value of 3. This corresponds to the Destination Unreachable class of ICMP messages, and the code value of 3 indicates that the Port Unreachable error has been caused. Refer to Figure for a diagram of a Destination Unreachable ICMP message.

Destination Unreachable ICMP message

As you can see, the first 8 bits are the type and the second 8 bits con- tain our ICMP code. One interesting thing to note is that when a host sends one of these ICMP messages, it actually includes the IP header of the originating message that generated the response. We can also see that we will double-check against 8 bytes of the original datagram that was sent in order to make sure our scanner generated the ICMP response. To do so, we simply slice off the last 8 bytes of the received buffer to pull out the magic string that our scanner sends.

Let’s add some more code to our previous sniffer to include the ability to decode ICMP packets. Let’s save our previous file as sniffer_with_icmp.py and add the following code:

--snip--
class IP(Structure):
--snip--
 class ICMP(Structure): 
 _fields_ = [
 ("type", c_ubyte),
 ("code", c_ubyte),
 ("checksum", c_ushort),
 ("unused", c_ushort),
 ("next_hop_mtu", c_ushort)
 ]
 def __new__(self, socket_buffer):
 return self.from_buffer_copy(socket_buffer) 
 def __init__(self, socket_buffer):
 pass
--snip--
 print "Protocol: %s %s -> %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

This simple piece of code creates an ICMP structure u underneath our existing IP structure.

# if it's ICMP, we want it
 if ip_header.protocol == "ICMP":

When the main packet-receiving loop determines that we have received an ICMP packet.

# calculate where our ICMP packet starts
 offset = ip_header.ihl * 4

We calculate the offset in the raw packet where the ICMP body lives.

buf = raw_buffer[offset:offset + sizeof(ICMP)]
 # create our ICMP structure
 icmp_header = ICMP(buf) 
 print "ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.
code)

Then create our buffer and print out the type and code fields. The length calculation is based on the IP header ihl field, which indicates the number of 32-bit words (4-byte chunks) contained in the IP header. So by multiplying this field by 4, we know the size of the IP header and thus when the next network layer— ICMP in this case—begins.

If we quickly run this code with our typical ping test, our output should now be slightly different, as shown below:

Protocol: ICMP 74.125.226.78 -> 192.168.0.190
ICMP -> Type: 0 Code: 0

This indicates that the ping (ICMP Echo) responses are being correctly received and decoded. We are now ready to implement the last bit of logic to send out the UDP datagrams, and to interpret their results.

Now let’s add the use of the netaddr module so that we can cover an entire subnet with our host discovery scan. Save your sniffer_with_icmp.py script as scanner.py and add the following code:

import threading
import time
from netaddr import IPNetwork,IPAddress
--snip--
# host to listen on
host = "192.168.0.187"
# subnet to target
subnet = "192.168.0.0/24"
# magic string we'll check ICMP responses for
 magic_message = "PYTHONRULES!"

This last bit of code should be fairly straightforward to understand. We define a simple string signature so that we can test that the responses are coming from UDP packets that we sent originally.

# this sprays out the UDP datagrams
 def udp_sender(subnet,magic_message): 
 time.sleep(5)
 sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 for ip in IPNetwork(subnet):
try:
 sender.sendto(magic_message,("%s" % ip,65212))
 except:
 pass
--snip--

Our udp_sender function simply takes in a subnet that we specify at the top of our script, iterates through all IP addresses in that subnet, and fires UDP datagrams at them.

# start sending packets
 t = threading.Thread(target=udp_sender,args=(subnet,magic_message)) 
t.start()
--snip--
try:
 while True:
 --snip--
 #print "ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.code)
 # now check for the TYPE 3 and CODE
 if icmp_header.code == 3 and icmp_header.type == 3:

In the main body of our script, just before the main packet decoding loop, we spawn udp_sender in a separate thread to ensure that we aren’t interfering with our ability to sniff responses.

# make sure host is in our target subnet 
 if IPAddress(ip_header.src_address) in IPNetwork(subnet):

If we detect the anticipated ICMP message, we first check to make sure that the ICMP response is com- ing from within our target subnet.

# make sure it has our magic message
 if raw_buffer[len(raw_buffer)-len(magic_message):] == magic_message: 
 print "Host Up: %s" % ip_header.src_address

We then perform our final check of making sure that the ICMP response has our magic string in it. If all of these checks pass, we print out the source IP address of where the ICMP message originated.

Now Let’s Check Our Code

Now let’s take our scanner and run it against the local network. You can use Linux or Windows for this as the results will be the same. In my case, the IP address of the local machine I was on was 192.168.0.187, so I set my scanner to hit 192.168.0.0/24. If the output is too noisy when you run your scanner, simply comment out all print statements except for the last one that tells you what hosts are responding.

c:\Python27\python.exe scanner.py
Host Up: 192.168.0.1
Host Up: 192.168.0.190
Host Up: 192.168.0.192
Host Up: 192.168.0.195

For a quick scan like the one I performed, it only took a few seconds to get the results back. By cross-referencing these IP addresses with the DHCP table in my home router, I was able to verify that the results were accurate. You can easily expand what you’ve learned in this chapter to decode TCP and UDP packets, and build additional tooling around it. This scanner is also useful for the trojan framework we will begin building in Chapter 7. This would allow a deployed trojan to scan the local network looking for additional targets. Now that we have the basics down of how networks work on a high and low level. In next article we will explore a very mature Python library called Scapy.

Note

The NETADDR Module

Our scanner is going to use a third-party library called netaddr, which will allow us to feed in a subnet mask such as 192.168.0.0/24 and have our scan- ner handle it appropriately. Download the library from here: http://code.google .com/p/netaddr/downloads/list

Or, if you installed the Python setup tools package, you can simply execute the following from a command prompt:

easy_install netaddr

The netaddr module makes it very easy to work with subnets and addressing. For example, you can run simple tests like the following using the IPNetwork object:

ip_address = "192.168.112.3"
if ip_address in IPNetwork("192.168.112.0/24"):
 print True

Or you can create simple iterators if you want to send packets to an entire network:

for ip in IPNetwork("192.168.112.1/24"):
 s = socket.socket()
 s.connect((ip, 25))
 # send mail packets

This will greatly simplify your programming life when dealing with entire networks at a time, and it is ideally suited for our host discovery tool. After it’s installed, you are ready to proceed.

Also Check Decoding The IP Layer With Python | For Hackers.

One thought on “Decoding The ICMP Packets With Python | For Hackers

Leave a Reply

Your email address will not be published. Required fields are marked *