How is an ICMP packet constructed in python

17,817

Solution 1

Description of ICMP Echo Request packets

The ICMP Echo Request PDU looks like this:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type(8)   |     Code(0)   |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             Payload                           |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

And here's a description of the various fields from the wiki link above:

The Identifier and Sequence Number can be used by the client to match the reply with the request that caused the reply.

In practice, most Linux systems use a unique identifier for every ping process, and sequence number is an increasing number within that process. Windows uses a fixed identifier, which varies between Windows versions, and a sequence number that is only reset at boot time.

Description of pyping Code

Header Generation

Look at the full function body of send_one_ping, which is where your code is from. I will annotate it with some information:

def send_one_ping(self, current_socket):
    """
    Send one ICMP ECHO_REQUEST
    """
    # Header is type (8), code (8), checksum (16), id (16), sequence (16)

    # Annotation: the Type is 8 bits, the code is 8 bits, the
    # header checksum is 16 bits

    # Additional Header Information is 32-bits (identifier and sequence number)

    # After that is Payload, which is of arbitrary length.

So this line

    header = struct.pack(
        "!BBHHH", ICMP_ECHO, 0, checksum, self.own_id, self.seq_number
    )

This line creates the packet header using struct with layout !BBHHH, which means:

  • B - Unsigned Char (8 bits)
  • B - Unsigned Char (8 bits)
  • H - Unsigned Short (16 bits)
  • H - Unsigned Short (16 bits)
  • H - Unsigned Short (16 bits)

And so the header will look like this:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     ICMP_ECHO  |     0        |          checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           self.own_id         |        self.seq_number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Note this:

  • self.own_id sets the identifier of the application sending this data. For this code, it just uses the program's Program Identifier number.
  • self.seq_number sets the sequence number. This helps you identify which ICMP request packet this is if you were to send multiple in a row. It would help you do things like calculate ICMP packet loss.

Both the Identifier and Sequence Number fields combined can be used by a client to match up echo replies with echo requests.

Payload Generation

Now let's move on to the Payload portion. Payloads are of arbitrary length, but the Ping class this code is taken from defaults to a total packet payload size of 55 bytes.

So the portion below just creates a bunch of arbitrary bytes to stuff into the payload section.

padBytes = []
startVal = 0x42

# Annotation: 0x42 = 66 decimal
# This loop would go from [66, 66 + packet_size],
# which in default pyping means [66, 121)
for i in range(startVal, startVal + (self.packet_size)):
    padBytes += [(i & 0xff)]  # Keep chars in the 0-255 range
data = bytes(padBytes)

At the end of it, byte(padBytes) actually looks like this:

>> bytes(padBytes)
b'BCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwx'

Why 0x42 was chosen?

As far as I know, 0x42 has no actual significance as a Payload identifier, so this seems rather arbitrary. The payload here is actually pretty meaningless. As you can see from the Payload Generation section, it just generates a contiguous sequence that doesn't really mean anything. They could have just decided to fill the entire packet payload with 0x42 bytes if they wanted.

Solution 2

Use scapy http://www.secdev.org/projects/scapy/ !

Scapy is packet manipulation framework written in python. You can forge a lot of kind of packets (http, tcp, ip, udp, icmp, etc...)

Solution 3

ICMP echo request and echo reply are better known as ping.

The ID and the sequence number allow to match the replies with the requests. They are necessary beacuse the ICMP protocol does not have source and destinations ports like TCP and UDP do, only a source and a destination IP address. Multiple processes can ping the same host and the replies must be delivered to the correct process. For this reason, each reply contains data literally copied from the corresponding request. The ID identifies the process sending the requests. The sequence number helps to match replies with requests within the process. That is necessary to calculate the RTT (round-trip time) and to detect unanswered pings.


The data computed in the loop is payload which is also literally copied from a request to a reply. The payload is optional and a ping implementation can use it for whatever it wants.


Why 0x42? I guess the author was probably a Douglas Adams fan.

Share:
17,817
SjoerdvdBelt
Author by

SjoerdvdBelt

I'm an unexperienced web developer currently working on my first two websites. I enjoy coding html, css and jquery.

Updated on July 24, 2022

Comments

  • SjoerdvdBelt
    SjoerdvdBelt almost 2 years

    For the sake of learning I am currently trying to create a simple python porgram to send a ICMP ping packet to some device. To get started I looked through the source code of the python module Pyping: https://github.com/Akhavi/pyping/blob/master/pyping/core.py

    I am trying to understand all that is going on when sending and constructing the packet however i have managed to get stuck on one part of the code and can't seem to figure out exactly what its fucntion and use is. I have been looking into ICMP packets and i understand that they contain Type code checksum and data now the piece of code that puzzles me is:

        self.own_id = os.getpid() & 0xFFFF
    
        header = struct.pack(
            "!BBHHH", ICMP_ECHO, 0, checksum, self.own_id, self.seq_number
        )
    
        padBytes = []
        startVal = 0x42
        for i in range(startVal, startVal + (self.packet_size)):
            padBytes += [(i & 0xff)]  # Keep chars in the 0-255 range
        data = bytes(padBytes)
    

    My questions would be:

    1. What is the use of adding the self.own_id and self.seq_number to the header?
    2. What is being calculated in the for-loop, and why does it have a specific start value of 0x42?

    I am new to networking and any help would be really appreciated.

  • SjoerdvdBelt
    SjoerdvdBelt over 8 years
    Thanks a lot! extremely well and thoroughly explained, this will help me out for sure, cheers!
  • wkl
    wkl over 8 years
    @SjoerdvdBelt you're welcome! I made some edits because I realized my side note was incorrect (I misread the description as saying the value of code was 8, but they were just detailing its memory usage), and I added some more details about the padBytes generation along with what the result looks like. Plus added some links to the Wiki article where I got the original payload layout description from.
  • RickS
    RickS about 3 years
    The use of '42' is a programmer idiom. It is the numerical equivalent of using 'foo' for giving an example of a variable name or a string content. I.e., authors are giving examples for which the actual words are arbitrary and would eventually be your choice, should you attempt to implement your own working code from example. In the case of '42' it traces its roots to the story "The Hitchhiker's Guide to the Galaxy" by D. Adams.