1

Тема: Сирі сокети. ICMP

Колись я роздумував "Як ж працює ping". Тоді ж шукаючи відповідь я надибав як працювати з Сирими сокетами.
Сирий сокет це тип сокету коли всі заголовки створюються вручну. Це цікава штука, але потребує root-прав.
Так багато писати я не люблю то поясню все на прикладі

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>

/* Функція для обчислення чексуми */
unsigned short csum(unsigned short *buf, int nwords)
{
    register unsigned long sum;
    for(sum=0; nwords>0; nwords--)
        sum += *buf++;
    sum = (sum >> 16) + (sum &0xffff);
    sum += (sum >> 16);
    return (unsigned short)(~sum);
}

/* Функція для визанчення власного IP */
int get_self_IP(in_addr_t addr, const int port)
{
    struct sockaddr_in serv;

    int sock = socket ( AF_INET, SOCK_DGRAM, 0);

    //Socket could not be created
    if(sock < 0)
    {
        return -1;
    }

    serv.sin_family      = AF_INET;
    serv.sin_addr.s_addr = addr;
    serv.sin_port        = htons(port);

    int err = connect( sock , (struct sockaddr*) &serv , sizeof(serv) );

    struct sockaddr_in name;
    socklen_t namelen = sizeof(name);
    err = getsockname(sock, (struct sockaddr*) &name, &namelen);

    close(sock);

    return name.sin_addr.s_addr;
}



int ICMP(in_addr_t addr, char* data, int datalen)
{
    /* Буфери для збереження даних */
    char outbuffer[1500];
    char inbuffer[1500];
    /* Налаштовуємо вказівники */
    struct iphdr *sendip  = (struct iphdr *) outbuffer,
                  *recvip = (struct iphdr *) inbuffer;
    struct icmphdr *sendicmp = (struct icmphdr *) (outbuffer + sizeof(struct iphdr)),
                    *recvicmp = (struct icmphdr *) (inbuffer + sizeof(struct iphdr));

    struct sockaddr_in sin, din;
    struct in_addr buff_for_addr;
    int one = 1;
    const int *val = &one;

    memset(outbuffer, 0, 1500);
    memset(inbuffer, 0, 1500);
    
    /* Створюємо сокет 
       SOCK_RAW - позначення сирого сокету
       IPPROTO_ICMP - тип пакету
    */
    int sd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
    if(sd < 0)
    {
        perror("socket() error");
        exit(-1);
    }
    else
        printf("socket() - Використання SOCK_RAW сокету і ICMP протокол OK.\n");

    sin.sin_family = AF_INET;
    din.sin_family = AF_INET;
    /* Налаштовуємо адреси */
    sin.sin_addr.s_addr = get_self_IP(addr, 80);
    din.sin_addr.s_addr = addr;
    
    /* Налаштовуємо IP заголовок */
    sendip->ihl      = 5;
    sendip->version  = 4;
    sendip->tos      = 0;
    sendip->tot_len  = sizeof(struct iphdr) + sizeof(struct icmphdr) + 6;
    sendip->frag_off = 0x40;
    sendip->id       = getpid();
    sendip->ttl      = 64;
    sendip->protocol = IPPROTO_ICMP; // Тип наступного заголовку
    sendip->saddr    = sin.sin_addr.s_addr;
    sendip->daddr    = din.sin_addr.s_addr;

    /* Налаштування ICMP заголовку */
    sendicmp->type   = ICMP_ECHO;
    sendicmp->code   = 0;
    sendicmp->un.echo.id = getpid();
    sendicmp->un.echo.sequence = 1;

    /* Записуємо передані дані в секцію даних */
    memcpy(outbuffer + sizeof(struct iphdr) + sizeof(struct icmphdr), data, datalen);

    /* Вказуємо, що ми самі "пишемо" заголовок */
    if(setsockopt(sd, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0)
    {
        perror("setsockopt()");
        exit(-1);
    }
    else
        printf("setsockopt() OK.\n");
    while(1)
    { 
        /* Записуємо чексуми, 
     я записую 0 з початку, бо в мене інакше змінні не перезаписувались*/
        sendip->check = 0;
        sendip->check = csum((unsigned short *)outbuffer, sizeof(struct iphdr) + sizeof(struct icmphdr) + datalen);
        sendicmp->checksum = 0;
        sendicmp->checksum = csum((unsigned short *)sendicmp, sizeof(struct icmphdr) + datalen);

    /* Надсилаємо пакет */
        if(sendto(sd, outbuffer, sendip->tot_len, 0, (struct sockaddr *)&din, sizeof(din)) < 0)
        {
            perror("sendto() error");
            exit(-1);
        }
        else
            printf("sendto() is OK.\n");

        /* Приймаємо пакет */
        int len = sizeof(din);
        if (recvfrom(sd, inbuffer, 1500, 0, (struct sockaddr *) &din, &len) < 0)
        {
            perror("recvfrom() error");
            exit(-1);
        }
        else
        {
        /* Перевіримо чи це наш пакет */
            if (recvicmp->un.echo.id == sendicmp->un.echo.id)
            {
                buff_for_addr.s_addr = recvip->saddr;
                printf("Package return from %s, seq = %d, ttl = %d\n", inet_ntoa(buff_for_addr),  recvicmp->un.echo.sequence, recvip->ttl);
                sendicmp->un.echo.sequence++;
            }
        }
    }
    close(sd);
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("- Invalid parameters!!!\n");
        printf("- Usage %s <target hostname/IP>\n", argv[0]);
        exit(-1);
    }
    in_addr_t addr;
    struct hostent *info;
    if (info = gethostbyname(argv[1]))  // Отримуємо дані про хост
    {
        addr = ((struct in_addr*)(info->h_addr_list[0]))->s_addr;
    }
    else
        addr = inet_addr(argv[1]);  // Якщо нічого не знайдено то записуєи IP як є
    ICMP(addr, "drWoZD", 6);

    return 0;
}

P.S. Сподіваюсь хтось дізнався щось нове :)
P.P.S. Якщо щось незрозуміло пишіть :)

2

Re: Сирі сокети. ICMP

Один мій друг сказав що коментів в коді недостатьньо, тому треба виправляти :)
Тож вибачаюсь перед всіма хто думає так само. :[
Як я написав в попередньому повідомлені при відправлені пакету сирим сокетом треба всі загаловки заповнити самостійно.
Перший з них це ІР заголовок. Він описаний в netinet/ip.h, і виглядає так

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error    "Please fix <bits/endian.h>"
#endif
    u_int8_t tos;
    u_int16_t tot_len;
    u_int16_t id;
    u_int16_t frag_off;
    u_int8_t ttl;
    u_int8_t protocol;
    u_int16_t check;
    u_int32_t saddr;
    u_int32_t daddr;
    /*The options start here. */
  };

Наступним заголовком є заголовок ICMP. Він описаний в netinet/ip_icmp.h і виглядає так

struct icmphdr
{
  u_int8_t type;        /* message type */
  u_int8_t code;        /* type sub-code */
  u_int16_t checksum;
  union
  {
    struct
    {
      u_int16_t    id;
      u_int16_t    sequence;
    } echo;            /* echo datagram */
    u_int32_t    gateway;    /* gateway address */
    struct
    {
      u_int16_t    __unused;
      u_int16_t    mtu;
    } frag;            /* path mtu discovery */
  } un;
};

Детальніше про заголовки можна прочитати на вікіпедії.

Йдемо далі. Щоб не треба було визначати IP-адресу сайту вручну я вставив цей фрагмент

in_addr_t addr;
    struct hostent *info;
    if (info = gethostbyname(argv[1]))  // Отримуємо дані про хост
    {
        addr = ((struct in_addr*)(info->h_addr_list[0]))->s_addr;
    }
    else
        addr = inet_addr(argv[1]); 

Функція gethostbyname приймає як параметр адресу і повертає вказівник на структуру hostent. Вона має поле s_addr - це набір адрес, які відповідають даному домену.
Далі в функції ICMP() оголошуються два буфери outbuffer і inbuffer, обидва довжиною 1500 байт.  Така довжина тому, що це максимальна довжина даних в ethernet фреймі. Потім я налаштовую всі вказівники.
При створені сокету як тип треба вказати SOCK_RAW. В якості протоколу теж можна вказати IPPROTO_RAW, але в даному випадку непотрібно, це б могло викликати зайві проблеми, оскільки сирий сокет працює як сніфер на рівні свого протоколу.
Структури sin і din потрібні для надсилання і отримування пакетів. В sin.sin_addr.s_add обовязково треба вказати свій IP (але якщо ви не хочете приймати пакет назад, а хочете просто закидати  когось фальшивими пакетами то можна вказати фальшивий IP).
Далі з ст 91-101 заповнюється заголовок IP, а з 103-107 Заповнюється заголовок ICMP. Після цього копіюються дані в середину пакету.
Виклик  setsockopt(sd, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) інформує систему що ІР заголовок створювався вручну.
Залишилось лише обчислити чексуми і можна відправляти. Після надсилання чекаємо на повернення відповіді, а так як сокет працює як сніфер то преревіряємо по id чи це наш пакет.

Ну от і все .
Код розрахований на компіляцію в будь якій UNIX системі. Для того щоб  скомпілити в ОС Форточка треба на початку main вставити

WSADATA wsaData;
int iResult;

// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
    printf("WSAStartup failed: %d\n", iResult);
    return 1;
}

всі типи сокетів з int заміни на SOCKET, а інклуди на початку замінити на

#include <stdlib.h>
#include <string.h>
#include <netinet/ip.h>            //тут щоправда не впевнений
#include <netinet/ip_icmp.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")

Але під віндою можуть бути проблеми, тому що в ній реалізація сирих сокетів обмежена