В этой статье речь пойдёт не столько о написании сниффера для Linux, сколько о структуре пакетов стека протоколов TCP/IP. Идея статьи взята из статьи на сайте xakep.ru.
Мы будем рассматривать пакеты, которые попадают к нам на сетевую карту. Эти пакеты относятся к протоколу Ethernet. Примерная структура этого пакета приведена в таблице ниже.

Ethernet заголовок
Ethernet данные (фрейм сетевого протокола)

IP заголовок
IP данные (фрейм транспортного протокола)

TCP или UDP заголовок
Данные приклодного протокола (HTTP, FTP, SMB итд).

Содержание статьи

Sniffer

Было бы не справедливо, если бы я приводил примеры анализа пакетов, не показав, как их получить. Для этой цели я написал сниффер, аналогичный тому, что описан в упомянутой статье.

#ifndef __LINUX_SNIFFER_SHIFFER_H__
#define __LINUX_SNIFFER_SHIFFER_H__


#include <set>
#include <string.h>
#include <stdexcept>
#include "Analyzer.h"


namespace LinuxSniffer {

class SnifferError :
public std::runtime_error
{
public:
SnifferError(const std::string & message) throw() :
std::runtime_error(message)
{
}

virtual ~SnifferError() throw()
{
}
}; // class SnifferError


class Sniffer
{
public:
explicit Sniffer(const std::string & device_name, bool sniff_all = false)
throw(SnifferError);
virtual ~Sniffer();
bool addAnalyzer(Analyzer * analyzer);
bool removeAnalyzer(Analyzer * analyzer);
void start() throw(SnifferError);
void stop();

private:
void deinit();
void makeSocket() throw(SnifferError);
void bindSocketToDevice() throw(SnifferError);
void setPromiscuousMode() throw(SnifferError);
void unsetPromiscuousMode();
void runAnalyzers(const unsigned char * frame, size_t frame_size);

private:
Sniffer(const Sniffer &);
Sniffer & operator = (const Sniffer &);

private:
const std::string m_device;
const bool m_sniff_all;
std::set<Analyzer *> m_analyzers;
int m_socket;
bool m_is_promiscuouse_mode_set;
bool m_is_stopping;
unsigned char * mp_frame_buffer;
static const size_t m_frame_buffer_size = 65536;
}; // class Sniffer


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_SHIFFER_H__

Когда сниффер получает пакет, он запускает всех зарегистрированных анализаторов. Для их регистрации и служат методы addAnalyzer и removeAnalyzer.
Каждый анализатор должен реализовывать интерфейс Analyzer

class Analyzer
{
public:
virtual ~Analyzer() { }
virtual void analyze(const uint8_t * frame, size_t frame_size) = 0;
}; // class Analyzer

Именно этим механизмом мы и будем пользоваться при анализе структуры пакетов стека TCP/IP.
Приведу полную реализацию класса Sniffer

#include <errno.h>
#include <netpacket/packet.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/sockios.h>
#include "Sniffer.h"


using namespace LinuxSniffer;


Sniffer::Sniffer(const std::string & device_name, bool sniff_all)
throw(SnifferError) :
m_device(device_name),
m_sniff_all(sniff_all),
m_is_promiscuouse_mode_set(false),
m_is_stopping(false),
mp_frame_buffer(0)
{
makeSocket();
mp_frame_buffer = new unsigned char[m_frame_buffer_size];
}

Sniffer::~Sniffer()
{
deinit();
}

void Sniffer::deinit()
{
::close(m_socket);
unsetPromiscuousMode();
delete [] mp_frame_buffer;
}

bool Sniffer::addAnalyzer(Analyzer * analyzer)
{
return m_analyzers.insert(analyzer).second;
}

bool Sniffer::removeAnalyzer(Analyzer * analyzer)
{
return m_analyzers.erase(analyzer) > 0;
}

void Sniffer::makeSocket()
throw(SnifferError)
{
m_socket = ::socket(AF_PACKET, SOCK_RAW, ::htons(ETH_P_ALL));
if(-1 == m_socket)
throw SnifferError(::strerror(errno));
try
{
bindSocketToDevice();
if(m_sniff_all)
setPromiscuousMode();
}
catch(...)
{
deinit();
throw;
}
}

void Sniffer::bindSocketToDevice()
throw(SnifferError)
{
const size_t device_name_len = m_device.length() + 1;
char * device = new char[device_name_len];
::strcpy(device, m_device.c_str());
device[m_device.length()] = '';
int setopt_result = ::setsockopt(m_socket, SOL_SOCKET,
SO_BINDTODEVICE, device, device_name_len);
delete [] device;
if(-1 == setopt_result)
throw SnifferError(::strerror(errno));
}

void Sniffer::setPromiscuousMode() throw(SnifferError)
{
ifreq iface;
::strcpy(iface.ifr_name, m_device.c_str());
if(::ioctl(m_socket, SIOCGIFFLAGS, &iface) < 0)
throw SnifferError(::strerror(errno));
iface.ifr_flags |= IFF_PROMISC;
if(::ioctl(m_socket, SIOCSIFFLAGS, &iface) < 0)
throw SnifferError(::strerror(errno));
m_is_promiscuouse_mode_set = true;
}

void Sniffer::unsetPromiscuousMode()
{
if(!m_is_promiscuouse_mode_set)
return;
ifreq iface;
::strcpy(iface.ifr_name, m_device.c_str());
if(::ioctl(m_socket, SIOCGIFFLAGS, &iface) >= 0)
{
iface.ifr_flags &= ~IFF_PROMISC;
if(::ioctl(m_socket, SIOCSIFFLAGS, &iface) >= 0)
m_is_promiscuouse_mode_set = false;
}
}

void Sniffer::start() throw(SnifferError)
{
while(!m_is_stopping)
{
ssize_t length = ::recvfrom(m_socket, mp_frame_buffer,
m_frame_buffer_size, 0, 0, 0);
if(-1 == length)
throw SnifferError(::strerror(errno));
runAnalyzers(mp_frame_buffer, length);
}
}

void Sniffer::runAnalyzers(const unsigned char * frame, size_t frame_size)
{
for(std::set<Analyzer *>::iterator it = m_analyzers.begin();
m_analyzers.end() != it; ++it)
{
Analyzer * analyzer = *it;
if(0 != analyzer)
analyzer->analyze(frame, frame_size);
}
}

void Sniffer::stop()
{
m_is_stopping = true;
}

Конструктор принимает два параметра: имя устройства для прослушивания, например eth0 и флаг, включающий неразборчивое прослушивание. Для тех сетей, которые работают через хабы, этот режим будет прослушивать все пакеты, даже те, которые не адресованы Вашей машине.
Конструктор вызывает метод makeSocket, который создаёт сокет для прослушивания устройства. Делается это вызовом

m_socket = ::socket(AF_PACKET, SOCK_RAW, ::htons(ETH_P_ALL));

Я отошёл от того вызова, что был показан в статье на xakep.ru по причине того, что в man 2 socket написано

SOCK_PACKET
Устарело и не должно использоваться в новых программах; см. packet(7).

А страница man 7 packet говорит, что нужно использовать SOCK_RAW.
Далее метод bindSocketToDevice привязывает созданный сокет к устройству вызовом

::setsockopt(m_socket, SOL_SOCKET, SO_BINDTODEVICE, device, device_name_len); 

Последним шагом в подготовке сокета является необязательная установка опции неразборчивого прослушивания методом setPromiscuousMode. Для её установки мы должны получить структуру ifreq из сокета вызовом

::ioctl(m_socket, SIOCGIFFLAGS, &iface)

добавить флаг IFF_PROMISC в поле ifr_flags

iface.ifr_flags |= IFF_PROMISC;

и записать сруктуру обратно

::ioctl(m_socket, SIOCSIFFLAGS, &iface)

Всё готово к запуску сниффера. Метод start вызывает в цикле функцию recvfrom и передаёт полученный буфер анализаторам.

ssize_t length = ::recvfrom(m_socket, mp_frame_buffer, m_frame_buffer_size, 0, 0, 0);

Стек протоколов TCP/IP

Прежде чем перейти к описанию анализатора, давайте рассмотрим фреймы протоколов стека TCP/IP.

Ethernet

Пакеты этого протокола являются низшими из тех, что мы можем получить. Существует несколько версий пакетов ethernet, мы рассмотрим самую популярную — вторую версию. В таблице ниже показана структура фрейма Ethernet 2

Смещение Размер Описание
0 байт 6 байт MAC адрес назначения
6 байт 6 байт MAC адрес источника
12 байт 2 байта Тип Etherner
14 байт 46 — 1500 байт Данные
Последние 4 байта 4 байта CRC контрольная сумма

Для разбора фреймов всех протоколов я написал базовый класс

#ifndef __LINUX_SNIFFER_PROTOCOL_FRAME_H__
#define __LINUX_SNIFFER_PROTOCOL_FRAME_H__

#include <string>
#include <sys/types.h>
#include <stdint.h>

namespace LinuxSniffer {


class ProtocolFrame
{
public:
ProtocolFrame(const std::string & protocol_name) :
m_protocol_name(protocol_name),
m_data_offset(0)
{
}

virtual ~ProtocolFrame()
{
}

const std::string & getName() const
{
return m_protocol_name;
}

virtual bool init(const uint8_t * buffer, size_t buffer_size) = 0;

size_t getDataOffset() const
{
return m_data_offset;
}

protected:
void setDataOffset(size_t offset)
{
m_data_offset = offset;
}

private:
const std::string m_protocol_name;
size_t m_data_offset;
}; // class ProtocolFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_PROTOCOL_FRAME_H__

Этот класс хранит и возвращает имя протокола описываемого фрейма и смещение данных от начала фрейма. Также класс передоставляет абстрактный метод инициализации из буфера фрейма и его размера.
Класс EthernetFrameV2 наследуется от ProtocolFrame и добавляет специфическую информацию.

#ifndef __LINUX_SNIFFER_ETHERNET_FRAME_V2_H__
#define __LINUX_SNIFFER_ETHERNET_FRAME_V2_H__

#include <cstring>
#include "ProtocolFrame.h"
#include "MacAddress.h"

namespace LinuxSniffer {

class EthernetFrameV2 :
public ProtocolFrame
{
public:
EthernetFrameV2() :
ProtocolFrame("Ethernet Version 2")
{
}

virtual ~EthernetFrameV2()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

const MacAddress & getSourceMacAddress() const
{
return m_src_mac_addr;
}

const MacAddress & getDestinationMacAddress() const
{
return m_dest_mac_addr;
}

static const uint8_t (& getEthernetType())[2]
{
static bool is_init = false;
static uint8_t type[2];
if(!is_init)
{
::memcpy(type, "x08x00", 2);
is_init = true;
}
return type;
}

private:
MacAddress m_src_mac_addr;
MacAddress m_dest_mac_addr;
}; // class EthernetFrameV2

} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_ETHERNET_FRAME_V2_H__

Дополнительные поля являются MAC адресами отправителя и получателя. Эти поля имеют тип MacAddress, который описан простым классом

#ifndef __LINUX_SNIFFER_MAC_ADDRESS_H__
#define __LINUX_SNIFFER_MAC_ADDRESS_H__

#include <stdint.h>
#include <string>
#include <cstdio>

namespace LinuxSniffer {


class MacAddress
{
public:
MacAddress() :
b0(0),
b1(0),
b2(0),
b3(0),
b4(0),
b5(0)
{
}

explicit MacAddress(const uint8_t address[6]) :
b0(address[0]),
b1(address[1]),
b2(address[2]),
b3(address[3]),
b4(address[4]),
b5(address[5])
{
}

std::string toString() const
{
char str[16];
::sprintf(str, "%02x:%02x:%02x:%02x:%02x:%02x", b0, b1, b2, b3, b4, b5);
return str;
}

public:
uint8_t b0;
uint8_t b1;
uint8_t b2;
uint8_t b3;
uint8_t b4;
uint8_t b5;
}; // class MacAddress


} // namespace LinuxSniffer



#endif // __LINUX_SNIFFER_MAC_ADDRESS_H__

Реализацтя метода init класса EthernetFrameV2 сводится к проверке версии протокола Ethernet и получению MAC адресов отправителя и получателя. Для простоты, получение контрольной суммы опустим. Для того, чтобы получить все необходимые поля, достаточно привести буфер фрейма к типу ether_header, объявленному в netinet/ether.h.

#include <cstring>
#include <netinet/ether.h>
#include "EthernetFrameV2.h"

using namespace LinuxSniffer;


bool EthernetFrameV2::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(ether_header))
return false;
const ether_header * hdr = reinterpret_cast<const ether_header *>(buffer);
if(::memcmp(&hdr->ether_type, getEthernetType(), sizeof(getEthernetType())))
return false;
setDataOffset(sizeof(ether_header));
m_src_mac_addr = MacAddress(hdr->ether_shost);
m_dest_mac_addr = MacAddress(hdr->ether_shost);
return true;
}

Internet Protocol

В секции данных фрейма протокола ethernet хранится фрейм протокола IP. Этот протокол добавляет к пакету информацию об IP адресах отправителя и получателя и множество служебных данных. В следующей таблице показана структура фрейма IP версии 4.

Байты Смещение в битах Размер в битах Описание
0 0 4 Версия
4 4 Размер заголовка
1 8 6 Точка кода дифференцированных услуг (Differentiated services code point)
14 2 Явное уведомление о перегруженности (Explicit congestion notification)
2-3 16 16 Размер пакета
4-5 32 16 Идентификатор
6-7 48 3 Флаги
51 13 Смещение фрагмента
8 64 8 Время жизни
9 72 8 Протокол
10 80 16 Контрольная сумма заголовка
11-15 96 32 IP адрес источника
16-20 128 32 IP адрес назначения
20-24 160 32 Опции (если размер заголовка > 5)
20+ или 24+ 160 или 192 Данные

В ранних версиях спецификации протокола «Точка кода дифференцированных услуг» и «Явное уведомление о перегруженности» были объединены в одно поле «Тип сервиса». Для того, чтобы использовать новое деление участники обмена должны договориться об этом. Я не буду разделять эти поля и буду использовать тип сервиса.
В поле «Флаги» биты от старшего к младшему означают:
0: Зарезервирован, должен быть равен 0;
1: Не фрагментировать;
2: У пакета еще есть фрагменты.
«Идентификатор» используется для предоставления информации о фрагментации пакета. Поле «Протокол» сообщает идентификатор протокола, фрейм которого находится в данных.
Думаю, остальные поля не нуждаются в комментариях.
Класс, описывающий IP фрейм приведён ниже

#ifndef __LINUX_SNIFFER_IP_FRAME_V4_H__
#define __LINUX_SNIFFER_IP_FRAME_V4_H__

#include <string>
#include "ProtocolFrame.h"

namespace LinuxSniffer {


class IpFrameV4 :
public ProtocolFrame
{
public:
IpFrameV4() :
ProtocolFrame("Internet Protocol Version 4"),
m_header_length(0),
m_tos(0),
m_package_size(0),
m_id(0),
m_flags(0),
m_frag_offset(0),
m_time_to_life(0),
m_protocol(0),
m_checksum(0)
{
}

virtual ~IpFrameV4()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

const std::string & getSourceAddress() const
{
return m_src_ip_addr;
}

const std::string & getDestinationAddress() const
{
return m_dest_ip_addr;
}

uint8_t getHeaderLength() const
{
return m_header_length;
}

uint8_t getTypeOfService() const
{
return m_tos;
}

uint16_t getPackageSize() const
{
return m_package_size;
}

uint16_t getId() const
{
return m_id;
}

uint8_t getFlags() const
{
return m_flags;
}

uint16_t getFragmintationOffset() const
{
return m_frag_offset;
}

uint8_t getTimeToLife() const
{
return m_time_to_life;
}

uint8_t getProtocolId() const
{
return m_protocol;
}

uint16_t getHeaderCheckSum() const
{
return m_checksum;
}

public:
static const uint8_t m_ipv4_version = 4;

private:
void splitFragmentOffsetAndFlags();

private:
std::string m_src_ip_addr;
std::string m_dest_ip_addr;
uint8_t m_header_length;
uint8_t m_tos;
uint16_t m_package_size;
uint16_t m_id;
uint8_t m_flags;
uint16_t m_frag_offset;
uint8_t m_time_to_life;
uint8_t m_protocol;
uint16_t m_checksum;
}; // class IpFrameV4


} // namespace LinuxSniffer



#endif // __LINUX_SNIFFER_IP_FRAME_V4_H__

Для того, чтобы получить все поля из буфера фрейма достаточно привести указатель на него к указателю на структуру iphdr, объявленной в файле netinet/ip.h. В реализации метода init класса IpFrameV4 именно так и сделано

#include <netinet/ip.h>
#include <arpa/inet.h>
#include "IpFrameV4.h"



using namespace LinuxSniffer;

bool IpFrameV4::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(iphdr) || 0 == buffer)
return false;
const iphdr * ip_header = reinterpret_cast<const iphdr *>(buffer);
if(m_ipv4_version != ip_header->version)
return false;
in_addr addr;
addr.s_addr = ip_header->saddr;
m_src_ip_addr = ::inet_ntoa(addr);
addr.s_addr = ip_header->daddr;
m_dest_ip_addr = ::inet_ntoa(addr);
m_header_length = ip_header->ihl;
m_tos = ip_header->tos;
m_package_size = ip_header->tot_len;
m_id = ip_header->id;
m_frag_offset = ip_header->frag_off;
m_time_to_life = ip_header->ttl;
m_protocol = ip_header->protocol;
m_checksum = ip_header->check;
setDataOffset(m_header_length);
splitFragmentOffsetAndFlags();
return true;
}

void IpFrameV4::splitFragmentOffsetAndFlags()
{
union
{
struct
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
uint16_t flags : 3;
uint16_t frag_offset : 13;
#elif __BYTE_ORDER == __BIG_ENDIAN
uint16_t frag_offset : 13;
uint16_t flags : 3;
#endif
} spl;
uint16_t num;
} splitter;

splitter.num = m_frag_offset;
m_flags = splitter.spl.flags;
m_frag_offset = splitter.spl.frag_offset;
}

Смещение фрагмента и флаги в структуре iphdr объединены. Для их разделения я написал метод splitFragmentOffsetAndFlags.

Transmission Control Protocol

Протоколы траспортного уровня добавляют сведения о портах источника и назначения. Для описания общих свойств всех фреймов транспортных протоколов я ввёл класс TransportProtocolFrame, унаследованный от ProtocolFrame.

#ifndef __LINUX_SNIFFER_TRANSPORT_PROTOCOL_FRAME_H__
#define __LINUX_SNIFFER_TRANSPORT_PROTOCOL_FRAME_H__

#include "ProtocolFrame.h"


namespace LinuxSniffer {


class TransportProtocolFrame :
public ProtocolFrame
{
public:
TransportProtocolFrame(const std::string & protocol_name) :
ProtocolFrame(protocol_name),
m_src_port(0),
m_dest_port(0)
{
}

virtual ~TransportProtocolFrame()
{
}

uint16_t getSourcePort() const
{
return m_src_port;
}

uint16_t getDestinationPort() const
{
return m_dest_port;
}

protected:
void setSourcePort(uint16_t port)
{
m_src_port = port;
}

void setDestinationPort(uint16_t port)
{
m_dest_port = port;
}

private:
uint16_t m_src_port;
uint16_t m_dest_port;
}; // class TransportProtocolFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_TRANSPORT_PROTOCOL_FRAME_H__

Структура фрейма протокола TCP, являющегося одним из протоколов транспортного уровня, показана в следующей таблице.

Байты Смещение в битах Размер в битах Описание
0-1 0 16 Порт источника
2-3 16 16 Порт назначения
4-8 32 32 Номер последовательности
8-12 64 32 Номер подтверждения последовательности
13-14 96 4 Смещение данных
100 6 Зарезервировано
106 6 Флаги
15-16 112 16 Размер окна
17-18 128 16 Контрольная сумма
19-20 144 16 Указатель важности
21+ 160 Необязательные опции
За опциями или 21+ За опциями или 160 Данные

Протокол TCP контролирует последовательность пакетов. Именно для этого нужны поля «Номер последовательности» и «Номер подтверждения последовательности».
«Флаги»:
URG — Поле «Указатель важности» задействовано;
ACK — Поле «Номер подтверждения» задействовано;
PSH — Инструктирует получателя протолкнуть данные, накопившиеся в приемном буфере, в приложение пользователя;
RST — Оборвать соединения;
SYN — Синхронизация номеров последовательности;
FIN — флаг указывает на завершение соединения.
«Размер окна» — это число байтов, которые получатель готов принять.
«Указатель важности» указывает на номер октета, которым заканчиваются важные данные. Это поле игнорируется, если флаг URG не установлен.
Для чтения фрейма протокола TCP я расширил класс TransportProtocolFrame классом TcpFrame.

#ifndef __LINUX_SNIFFER_TCP_FRAME_H__
#define __LINUX_SNIFFER_TCP_FRAME_H__

#include "TransportProtocolFrame.h"

namespace LinuxSniffer {


class TcpFrame :
public TransportProtocolFrame
{
public:
TcpFrame() :
TransportProtocolFrame("Transmission Control Protocol"),
m_sequence_number(0),
m_ascknowledgment_number(0),
m_flag_fin(false),
m_flag_syn(false),
m_flag_rst(false),
m_flag_psh(false),
m_flag_ack(false),
m_flag_urg(false),
m_window_size(0),
m_checksum(0),
m_urgent_ptr(0)
{
}

virtual ~TcpFrame()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

uint32_t getSequenceNumber() const
{
return m_sequence_number;
}

uint32_t getAscknowledgmentNumber() const
{
return m_ascknowledgment_number;
}

bool isFinFlagSet() const
{
return m_flag_fin;
}

bool isSynFlagSet() const
{
return m_flag_syn;
}

bool isRstFlagSet() const
{
return m_flag_rst;
}

bool isPshFlagSet() const
{
return m_flag_psh;
}

bool isAckFlagSet() const
{
return m_flag_ack;
}

bool isUrgFlagSet() const
{
return m_flag_urg;
}

uint16_t getWindowSize() const
{
return m_window_size;
}

uint16_t getCheckSum() const
{
return m_checksum;
}

uint16_t getUrgentPtr() const
{
return m_urgent_ptr;
}

public:
static const uint8_t m_protocol_id = 6;

private:
uint32_t m_sequence_number;
uint32_t m_ascknowledgment_number;
bool m_flag_fin;
bool m_flag_syn;
bool m_flag_rst;
bool m_flag_psh;
bool m_flag_ack;
bool m_flag_urg;
uint16_t m_window_size;
uint16_t m_checksum;
uint16_t m_urgent_ptr;
}; // class TcpFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_TCP_FRAME_H__

Как и в случае с IP, для фрейма TCP в заголовочных файлах linux припасена структура tcphdr. Находится она в файле netinet/tcp.h. Реализуем метод init с использованием этой структуры.

#include <netinet/tcp.h>
#include "TcpFrame.h"

using namespace LinuxSniffer;


bool TcpFrame::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(tcphdr))
return false;
const tcphdr * tcp_header = reinterpret_cast<const tcphdr *>(buffer);
setSourcePort(tcp_header->source);
setDestinationPort(tcp_header->dest);
setDataOffset(tcp_header->doff);
m_sequence_number = tcp_header->seq;
m_ascknowledgment_number = tcp_header->ack_seq;
m_flag_fin = tcp_header->fin != 0;
m_flag_syn = tcp_header->syn != 0;
m_flag_rst = tcp_header->rst != 0;
m_flag_psh = tcp_header->psh != 0;
m_flag_ack = tcp_header->ack != 0;
m_flag_urg = tcp_header->urg != 0;
m_window_size = tcp_header->window;
m_checksum = tcp_header->check;
m_urgent_ptr = tcp_header->urg_ptr;
return true;
}

User Datagram Protocol

Последний протокол, который мы рассмотрим в этой статье — UDP. Это простой протокол транспортного уровня, который ни как не контролирует доставку пакетов и не устанавливает соединения. По этим причинам, фрейм этого протокола весьма прост.

Байты Смещение в битах Размер в битах Описание
0-1 0 16 Порт источника
2-3 16 16 Порт назначения
4-5 32 16 Длина дейтаграммы
6-7 48 16 Контрольная сумма
8+ 64 Данные

В этой таблице всё понятно без коментариев.
Второй класс, унаследованный от TransportProtocolFrame — UdpFrame.

#ifndef __LINUX_SNIFFER_UDP_FRAME_H__
#define __LINUX_SNIFFER_UDP_FRAME_H__

#include "TransportProtocolFrame.h"

namespace LinuxSniffer {


class UdpFrame :
public TransportProtocolFrame
{
public:
UdpFrame() :
TransportProtocolFrame("User Datagram Protocol")
{
}

virtual ~UdpFrame()
{
}

virtual bool init(const uint8_t * buffer, size_t buffer_size);

uint16_t getDatagramLength() const
{
return m_datagram_length;
}

uint16_t getCheckSum() const
{
return m_checksum;
}

public:
static const uint8_t m_protocol_id = 17;

private:
uint16_t m_datagram_length;
uint16_t m_checksum;
}; // class UdpFrame


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_UDP_FRAME_H__

Аналогично фреймам протоколов IP и TCP, реализуем метод init с использованием структуры udphdr, объявленной в файле netinet/udp.h.

#include <netinet/udp.h>
#include "UdpFrame.h"

using namespace LinuxSniffer;


bool UdpFrame::init(const uint8_t * buffer, size_t buffer_size)
{
if(buffer_size < sizeof(udphdr))
return false;
const udphdr * udp_header = reinterpret_cast<const udphdr *>(buffer);
setSourcePort(udp_header->source);
setDestinationPort(udp_header->dest);
setDataOffset(sizeof(udphdr));
m_datagram_length = udp_header->len;
m_checksum = udp_header->check;
return true;
}

Анализатор

Теперь, когда мы знаем структуру основных протоколов, ничего не стоит проанализировать данные, которые мы получаем от сниффера. Для этого реализуем интерфейс Analyzer.

#ifndef __LINUX_SNIFFER_IP_ANALYZER_H__
#define __LINUX_SNIFFER_IP_ANALYZER_H__

#include <utility>
#include "Analyzer.h"


namespace LinuxSniffer {

class TransportProtocolFrame;

class IpAnalyzer :
public Analyzer
{
public:
virtual ~IpAnalyzer() { }
virtual void analyze(const uint8_t * frame, size_t frame_size);

private:
size_t tryEthV2Analyze(const uint8_t * frame, size_t frame_size);
std::pair<size_t, uint8_t> tryIpV4Analyze(const uint8_t * frame, ize_t frame_size);
size_t tryTcpAnalyze(const uint8_t * frame, size_t frame_size);
size_t tryUdpAnalyze(const uint8_t * frame, size_t frame_size);
void printTransportProtocolFrame(const TransportProtocolFrame & frame) const;
}; // class IpAnalyzer


} // namespace LinuxSniffer


#endif // __LINUX_SNIFFER_IP_ANALYZER_H__
#include <iostream>
#include "IpAnalyzer.h"
#include "EthernetFrameV2.h"
#include "IpFrameV4.h"
#include "TcpFrame.h"
#include "UdpFrame.h"

using namespace LinuxSniffer;

void IpAnalyzer::analyze(const uint8_t * frame, size_t frame_size)
{
const size_t ip_offset = tryEthV2Analyze(frame, frame_size);
if(0 == ip_offset)
return;

const uint8_t * ip_frame = &frame[ip_offset];
const size_t ip_frame_size = frame_size - ip_offset;
std::pair<size_t, uint8_t> ip_analyze_result =
tryIpV4Analyze(ip_frame, ip_frame_size);
if(0 == ip_analyze_result.first)
return;


const uint8_t * transport_frame = ip_frame + ip_analyze_result.first;
const size_t transport_frame_size = ip_frame_size - ip_analyze_result.first;
size_t application_protocol_data_offset;
switch(ip_analyze_result.second)
{
case TcpFrame::m_protocol_id:
application_protocol_data_offset = tryTcpAnalyze(
transport_frame, transport_frame_size);
break;
case UdpFrame::m_protocol_id:
application_protocol_data_offset = tryUdpAnalyze(
transport_frame, transport_frame_size);
break;
default:
std::cout << "======= Unsupported transport protocol ======n";
}

std::cout << std::endl;
}

size_t IpAnalyzer::tryEthV2Analyze(const uint8_t * frame, size_t frame_size)
{
EthernetFrameV2 eth_frame;
if(!eth_frame.init(frame, frame_size))
return 0;

std::cout << "====== " << eth_frame.getName() << " ======n" <<
"Source MAC Address: " << eth_frame.getSourceMacAddress().toString() <<
std::endl << "Destination MAC Address: " <<
eth_frame.getDestinationMacAddress().toString() << std::endl;

return eth_frame.getDataOffset();
}

std::pair<size_t, uint8_t> IpAnalyzer::tryIpV4Analyze(const uint8_t * frame,
size_t frame_size)
{
IpFrameV4 ip_frame;
if(!ip_frame.init(frame, frame_size))
return std::make_pair(0, 0);

std::cout << "====== " << ip_frame.getName() << " ======n" <<
"Source IP Address: " << ip_frame.getSourceAddress() << std::endl <<
"Destination IP Address: " << ip_frame.getDestinationAddress() <<
std::endl <<
"Header Length: " << std::dec <<
static_cast<uint32_t>(ip_frame.getHeaderLength()) << std::endl <<
"Type Of Service: " <<
static_cast<uint32_t>(ip_frame.getTypeOfService())<< std::endl <<
"Package Size: "<< ip_frame.getPackageSize() << std::endl <<
"Identification: " << ip_frame.getId() << std::endl <<
"Flags: " << static_cast<uint32_t>(ip_frame.getFlags())<< std::endl <<
"Fragmentation Offset: " << ip_frame.getFragmintationOffset() <<
std::endl <<
"Time To Live: " << static_cast<uint32_t>(ip_frame.getTimeToLife()) <<
std::endl <<
"Transport Protocol ID: " <<
static_cast<uint32_t>(ip_frame.getProtocolId()) << std::endl <<
"CRC-16 Header CheckSum:" << std::hex << ip_frame.getHeaderCheckSum() <<
std::endl;
return std::make_pair(ip_frame.getDataOffset(), ip_frame.getProtocolId());
}

size_t IpAnalyzer::tryTcpAnalyze(const uint8_t * frame, size_t frame_size)
{
TcpFrame tcp_frame;
if(!tcp_frame.init(frame, frame_size))
return 0;

printTransportProtocolFrame(tcp_frame);

std::cout << std::dec <<
"Sequence Number: " << tcp_frame.getSequenceNumber() << std::endl <<
"Ascknowledgment Number: " << tcp_frame.getAscknowledgmentNumber() <<
std::endl <<
"Window Size: " << tcp_frame.getWindowSize() << std::endl <<
"Urgent Pointer: " << tcp_frame.getUrgentPtr() << std::endl <<
"Flags:n" <<
" FIN: " << std::boolalpha << tcp_frame.isFinFlagSet() << std::endl <<
" SYN: " << tcp_frame.isSynFlagSet() << std::endl <<
" RST: " << tcp_frame.isRstFlagSet() << std::endl <<
" PSH: " << tcp_frame.isPshFlagSet() << std::endl <<
" ACK: " << tcp_frame.isAckFlagSet() << std::endl <<
" URG: " << tcp_frame.isUrgFlagSet() << std::endl <<
"CheckSum: " << std::hex << tcp_frame.getCheckSum() << std::endl;

return tcp_frame.getDataOffset();
}

size_t IpAnalyzer::tryUdpAnalyze(const uint8_t * frame, size_t frame_size)
{
UdpFrame udp_frame;
if(!udp_frame.init(frame, frame_size))
return 0;

printTransportProtocolFrame(udp_frame);
std::cout << std::dec <<
"Datagram Length: " << std::dec << udp_frame.getDatagramLength() <<
std::endl <<
"Datagram CheckSum: " << std::hex << udp_frame.getCheckSum() <<
std::endl;
return udp_frame.getDataOffset();
}

void IpAnalyzer::printTransportProtocolFrame(
const TransportProtocolFrame & frame) const
{
std::cout << "====== " << frame.getName() << " ======n" <<
std::dec << "Source Port: " << frame.getSourcePort() << std::endl <<
"Destination Port: " << frame.getDestinationPort() <<
std::endl;
}

Всё очень просто. Пытаемся преобразовать полученный фрейм в фрейм протокола Ethernet версии 2. Затем, используя полученное смещение данных, получаем фрейм IP. Проанализировав номер протокола, пытаемся получить данные либо о TCP, либо о UDP фрейме. Попутно выводим всю полученную информацию на консоль. В конце анализа у нас остаётся переменная, хранящая смещение данных прикладного протокола, которую Вы можете применить для дальнейшего разворачивания стека.

Заключение

Полученную программу нельзя запустить от пользователя, чей UID не равен нулю. Это и естественно, ведь мы слишком много себе позволили.
Исходные тексты программы можно скачать здесь.