Nov
24
2011

Code a network packet sniffer in PHP

Example of a packet sniffer is Wireshark. Packet sniffers pick up the packets going in and out of a system and analyse them and present them to user for further analysis.

In this post we are going to code a simple packet sniffer in php. The basic theory this packet sniffer is that , raw packets can sniff without much effort if they are put into receiving mode. This will work only on a Linux system , since we cannot create raw sockets on windows.

So the steps are :

1. Create a raw socket
2. Receive on it.
3. See what you received.

Code

<?php
/**
	Packet sniffer in PHP
  	Will run only on Linux
  	Needs root privileges , so use sudo !!
*/

error_reporting(~E_ALL);

//Create a RAW socket
$socket = socket_create(AF_INET , SOCK_RAW , SOL_TCP);
if($socket)
{
	echo "Starting sniffing...\n";
  	while(true)
  	{
    	//Start receiving on the raw socket
    	socket_recv ( $socket , &$buf , 65536 , 0 );

 		//Process the packet
    	process_packet($buf);
  	}
}

//Some error - check that you used sudo !!
else
{
	$error_code = socket_last_error();
  	$error_message = socket_strerror($error_code);  

  	echo "Could not create socket : [$error_code] $error_message";
}

/**
	Process the captured packet.
*/
function process_packet($packet)
{
	//IP Header
  	$ip_header_fmt = 'Cip_ver_len/'
	.'Ctos/'
	.'ntot_len/'
	.'nidentification/'
	.'nfrag_off/'
	.'Cttl/'
	.'Cprotocol/nheader_checksum/Nsource_add/Ndest_add/';

	//Unpack the IP header
  	$ip_header = unpack($ip_header_fmt , $packet);

	if($ip_header['protocol'] == '6')
  	{
    	print_tcp_packet($packet);
  	}
}

/*
  Process a TCP Packet :)
*/
function print_tcp_packet($packet)
{
	$ip_header_fmt = 'Cip_ver_len/'
	.'Ctos/'
	.'ntot_len/';

	$p = unpack($ip_header_fmt , $packet);
	$ip_len = ($p['ip_ver_len'] & 0x0F);

	if($ip_len == 5)
	{

		//IP Header format for unpack
		$ip_header_fmt = 'Cip_ver_len/'
		.'Ctos/'
		.'ntot_len/'
		.'nidentification/'
		.'nfrag_off/'
		.'Cttl/'
		.'Cprotocol/'
		.'nip_checksum/'
		.'Nsource_add/'
		.'Ndest_add/';
  	}
  	else if ($ip_len == 6)
  	{
  		//IP Header format for unpack
		$ip_header_fmt = 'Cip_ver_len/'
		.'Ctos/'
		.'ntot_len/'
		.'nidentification/'
		.'nfrag_off/'
		.'Cttl/'
		.'Cprotocol/'
		.'nip_checksum/'
		.'Nsource_add/'
		.'Ndest_add/'
		.'Noptions_padding/';
  	}

  	$tcp_header_fmt = 'nsource_port/'
	.'ndest_port/'
	.'Nsequence_number/'
	.'Nacknowledgement_number/'
	.'Coffset_reserved/';

  	//total packet unpack format
  	$total_packet = $ip_header_fmt.$tcp_header_fmt.'H*data';

  	$p = unpack($total_packet , $packet);
	$tcp_header_len = ($p['offset_reserved'] >> 4);

	if($tcp_header_len == 5)
	{
		//TCP Header Format for unpack
		$tcp_header_fmt = 'nsource_port/'
		.'ndest_port/'
		.'Nsequence_number/'
		.'Nacknowledgement_number/'
		.'Coffset_reserved/'
		.'Ctcp_flags/'
		.'nwindow_size/'
		.'nchecksum/'
		.'nurgent_pointer/';
	}
  	else if($tcp_header_len == 6)
  	{
		//TCP Header Format for unpack
		$tcp_header_fmt = 'nsource_port/'
		.'ndest_port/'
		.'Nsequence_number/'
		.'Nacknowledgement_number/'
		.'Coffset_reserved/'
		.'Ctcp_flags/'
		.'nwindow_size/'
		.'nchecksum/'
		.'nurgent_pointer/'
		.'Ntcp_options_padding/';
  	}

  	//total packet unpack format
  	$total_packet = $ip_header_fmt.$tcp_header_fmt.'H*data';

	//unpack the packet finally
  	$packet = unpack($total_packet , $packet);

  	//prepare the unpacked data
	$sniff = array(

		'ip_header' => array(
			'ip_ver' => ($packet['ip_ver_len'] >> 4) ,
			'ip_len' => ($packet['ip_ver_len'] & 0x0F) ,
			'tos' => $packet['tos'] ,
			'tot_len' => $packet['tot_len'] ,
			'identification' => $packet['identification'] ,
			'frag_off' => $packet['frag_off'] ,
			'ttl' => $packet['ttl'] ,
			'protocol' => $packet['protocol'] ,
			'checksum' => $packet['ip_checksum'] ,
			'source_add' => long2ip($packet['source_add']) ,
			'dest_add' => long2ip($packet['dest_add']) ,
		) ,

		'tcp_header' => array(
			'source_port' => $packet['source_port'] ,
			'dest_port' => $packet['dest_port'] ,
			'sequence_number' => $packet['sequence_number'] ,
			'acknowledgement_number' => $packet['acknowledgement_number'] ,
			'tcp_header_length' => ($packet['offset_reserved'] >> 4) ,

			'tcp_flags' => array(
				'cwr' => (($packet['tcp_flags'] & 0x80) >> 7) ,
				'ecn' => (($packet['tcp_flags'] & 0x40) >> 6) ,
				'urgent' => (($packet['tcp_flags'] & 0x20) >> 5 ) ,
				'ack' => (($packet['tcp_flags'] & 0x10) >>4) ,
				'push' => (($packet['tcp_flags'] & 0x08)>>3) ,
				'reset' => (($packet['tcp_flags'] & 0x04)>>2) ,
				'syn' => (($packet['tcp_flags'] & 0x02)>>1) ,
				'fin' => (($packet['tcp_flags'] & 0x01)) ,
			) ,

			'window_size' => $packet['window_size'] ,
			'checksum' => $packet['checksum'] . ' [0x'.dechex($packet['checksum']).']',
		) ,

  		'data' => hex_to_str($packet['data'])
	);

	//print the unpacked data
	print_r($sniff);
}

/*
	idea taken from http://ditio.net/2008/11/04/php-string-to-hex-and-hex-to-string-functions/
	modified a bit to show non alphanumeric characters as dot.
*/
function hex_to_str($hex)
{
    $string='';

    for ($i=0; $i < strlen($hex)-1; $i+=2)
    {
        $d = hexdec($hex[$i].$hex[$i+1]);

        //Show only if number of alphabet
        if( ($d >= 48 and $d <= 57) or ($d >= 65 and $d <= 90) or ($d >= 97 and $d <= 122) )
        {
        	$string .= chr(hexdec($hex[$i].$hex[$i+1]));
        }
        else
        {
        	$string .= '.';
        }
    }

    return $string;
}

The script needs to run with root privileges. So on ubuntu you can run like this :
sudo php sniffer.php

Analysis

This is what creates a raw socket

$socket = socket_create(AF_INET , SOCK_RAW , SOL_TCP);

SOCK_RAW means raw.

No start receiving on the raw socket.

while(true)
  	{
    	//Start receiving on the raw socket
    	socket_recv ( $socket , &$buf , 65536 , 0 );

 		//Process the packet
    	process_packet($buf);
  	}

The socket_recv receives some passing by packet in $buf. Then process_packet will analyse the packet which is in $buf.

The structure of a typical TCP packet is like this : IP Header + TCP Header + Data
So the packet needs to be broken down into the relevant parts.

IP Header
According to RFC 791 the IP headers 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The IHL (IP Header Length) contains the length of the ip header / 4. So if its value is 5 then header length is 20 bytes. Generally header length is 5 and options and padding is not there. If options and padding is present then length would be 6. This has to be checked first before moving to the next part that is TCP header.

Since the $buf contains data in binary format , it needs to be unpacked using the unpack function of php. The unpack string should be:

//IP Header format for unpack
		$ip_header_fmt = 'Cip_ver_len/'
		.'Ctos/'
		.'ntot_len/'
		.'nidentification/'
		.'nfrag_off/'
		.'Cttl/'
		.'Cprotocol/'
		.'nip_checksum/'
		.'Nsource_add/'
		.'Ndest_add/';

For TCP packets the value of protocol is 6.

TCP headers 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|P|R|S|F|                               |
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |
|       |           |G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The TCP header also has an options and padding part. The TCP header length is present in the Data Offset part and needs to be multiplied by 4 to get the length in bytes.

They can be unpacked with this string :

//TCP Header Format for unpack
		$tcp_header_fmt = 'nsource_port/'
		.'ndest_port/'
		.'Nsequence_number/'
		.'Nacknowledgement_number/'
		.'Coffset_reserved/'
		.'Ctcp_flags/'
		.'nwindow_size/'
		.'nchecksum/'
		.'nurgent_pointer/';

The print_tcp_packet breaks down the tcp packet into its parts and saves them in a array with proper key names for analysis.

//total packet unpack format
  	$total_packet = $ip_header_fmt.$tcp_header_fmt.'H*data';

	//unpack the packet finally
  	$packet = unpack($total_packet , $packet);

  	//prepare the unpacked data
	$sniff = array(

		'ip_header' => array(
			'ip_ver' => ($packet['ip_ver_len'] >> 4) ,
			'ip_len' => ($packet['ip_ver_len'] & 0x0F) ,
			'tos' => $packet['tos'] ,
			'tot_len' => $packet['tot_len'] ,
			'identification' => $packet['identification'] ,
			'frag_off' => $packet['frag_off'] ,
			'ttl' => $packet['ttl'] ,
			'protocol' => $packet['protocol'] ,
			'checksum' => $packet['ip_checksum'] ,
			'source_add' => long2ip($packet['source_add']) ,
			'dest_add' => long2ip($packet['dest_add']) ,
		) ,

		'tcp_header' => array(
			'source_port' => $packet['source_port'] ,
			'dest_port' => $packet['dest_port'] ,
			'sequence_number' => $packet['sequence_number'] ,
			'acknowledgement_number' => $packet['acknowledgement_number'] ,
			'tcp_header_length' => ($packet['offset_reserved'] >> 4) ,

			'tcp_flags' => array(
				'cwr' => (($packet['tcp_flags'] & 0x80) >> 7) ,
				'ecn' => (($packet['tcp_flags'] & 0x40) >> 6) ,
				'urgent' => (($packet['tcp_flags'] & 0x20) >> 5 ) ,
				'ack' => (($packet['tcp_flags'] & 0x10) >>4) ,
				'push' => (($packet['tcp_flags'] & 0x08)>>3) ,
				'reset' => (($packet['tcp_flags'] & 0x04)>>2) ,
				'syn' => (($packet['tcp_flags'] & 0x02)>>1) ,
				'fin' => (($packet['tcp_flags'] & 0x01)) ,
			) ,

			'window_size' => $packet['window_size'] ,
			'checksum' => $packet['checksum'] . ' [0x'.dechex($packet['checksum']).']',
		) ,

  		'data' => hex_to_str($packet['data'])
	);

Similar piece of code can be written for UDP packets , ICMP packets and so on.
The C version of the same code can be found here : http://www.binarytides.com/blog/packet-sniffer-code-in-c-using-linux-sockets-bsd/.

Popularity: 3% [?]

2 Comments + Add Comment

  • Thanks for posting this!

    I am trying to run it, but can’t create the socket. How do you specify to run it as sudo to view/run the script in a browser (I have it saved in: htdocs/sniff/sniffer.php)? Your suggestion of “sudo php sniffer.php” from the command line results in a command not found msg.

    Could you please let me know how you are running this awesome script?

    Thanks!

    • you need php cli to be installed.
      Check it with the command “php -v”
      It should show something like this :

      PHP 5.3.5-1ubuntu7.4 with Suhosin-Patch (cli) (built: Dec 13 2011 18:30:11)
      Copyright (c) 1997-2009 The PHP Group
      Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies
      with Xdebug v2.1.0, Copyright (c) 2002-2010, by Derick Rethans

      If it says that command not found then its not installed.

      Also this code will work only on Linux.

      When running the script provide the full path like :

      sudo php /var/htdocs/sniff/sniffer.php

      or something similar

Leave a comment