Các loại socket thông thường chỉ cho phép ứng dụng truy cập vào payload của tầng transport do hệ điều hành chịu trách nhiệm tạo các header cho tầng transport, network và data link. Để có thể truy cập và chỉnh sửa các header thuộc tầng network và data link, chúng ta cần sử dụng raw socket.

Có hai loại raw socket tương ứng với tầng network (L3 socket) và tầng data link (L2 socket).

Seealso

Nếu muốn lập trình raw socket trên Windows thì cần dùng thư viện winpcap.

Opening a Raw Socket

Để mở một socket, ta cần xác định họ socket, loại socket và giao thức rồi truyền vào hàm socket() (sys/socket.h):

int open_raw_socket(int sock_family, int sock_type, int protocol)
{
    int raw_socket = socket(sock_family, sock_type, protocol);
    if (raw_socket < 0)
    {
        perror("Error: socket() failed");
        return -1;
    }
    return raw_socket;
}

Giá trị trả về của hàm socket() là một socket descriptor có kiểu là int.

Với raw socket ở layer 3:

  • Họ socket là AF_INET (bits/socket.h).
  • Loại socket là SOCK_RAW (bits/socket_types.h).
  • Loại giao thức là IPPROTO_RAW (netinet/in.h).

Ví dụ:

/* Open layer-3 raw socket */
int layer3_socket_descriptor = open_raw_socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

Còn đối với raw socket ở layer 2:

  • Họ socket là AF_PACKET
  • Loại socket là SOCK_RAW
  • Loại giao thức là các macro tronglinux/if_ether.h. Macro cho giao thức IPETH_P_IP, tương ứng với giá trị 0x0800. Ví dụ:
/* Open layer-2 raw socket*/
int layer2_socket_descriptor = open_raw_socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));

Info

Hàm htons() (netinet/in.h) giúp chuyển thứ tự byte của host thành thứ tự byte của gói tin mạng.

Interface Request

Để truy xuất đến các thuộc tính của network interface, ta cần dùng cấu trúc ifreq (linux/if.h):

struct ifreq
{
#define IFHWADDRLEN 6
    union
    {
        char ifrn_name[IFNAMSIZ]; /* if name, e.g. "en0" */
    } ifr_ifrn;
 
    union
    {
        struct sockaddr ifru_addr;
        struct sockaddr ifru_dstaddr;
        struct sockaddr ifru_broadaddr;
        struct sockaddr ifru_netmask;
        struct sockaddr ifru_hwaddr;
        short ifru_flags;
        int ifru_ivalue;
        int ifru_mtu;
        struct ifmap ifru_map;
        char ifru_slave[IFNAMSIZ]; /* Just fits the size */
        char ifru_newname[IFNAMSIZ];
        void *ifru_data;
        struct if_settings ifru_settings;
    } ifr_ifru;
};
 
#define ifr_name ifr_ifrn.ifrn_name           /* interface name 	*/
#define ifr_hwaddr ifr_ifru.ifru_hwaddr       /* MAC address 		*/
#define ifr_addr ifr_ifru.ifru_addr           /* address		*/
#define ifr_dstaddr ifr_ifru.ifru_dstaddr     /* other end of p-p lnk	*/
#define ifr_broadaddr ifr_ifru.ifru_broadaddr /* broadcast address	*/
#define ifr_netmask ifr_ifru.ifru_netmask     /* interface net mask	*/
#define ifr_flags ifr_ifru.ifru_flags         /* flags		*/
#define ifr_metric ifr_ifru.ifru_ivalue       /* metric		*/
#define ifr_mtu ifr_ifru.ifru_mtu             /* mtu			*/
#define ifr_map ifr_ifru.ifru_map             /* device map		*/
#define ifr_slave ifr_ifru.ifru_slave         /* slave device		*/
#define ifr_data ifr_ifru.ifru_data           /* for use by interface	*/
#define ifr_ifindex ifr_ifru.ifru_ivalue      /* interface index	*/
#define ifr_bandwidth ifr_ifru.ifru_ivalue    /* link bandwidth	*/
#define ifr_qlen ifr_ifru.ifru_ivalue         /* Queue length 	*/
#define ifr_newname ifr_ifru.ifru_newname     /* New name		*/
#define ifr_settings ifr_ifru.ifru_settings   /* Device/proto settings*/

Xây dựng hàm khởi tạo cấu trúc như sau:

struct ifreq initialize_ifreq(char *if_name)
{
    struct ifreq if_request;
    memset(&if_request, 0, sizeof(struct ifreq));
    strncpy(if_request.ifr_name, if_name, strlen(if_name));
    return if_request;
}

Với if_name là tên network interface mà ta muốn truy xuất thông tin.

Sử dụng như sau:

struct ifreq if_req = initialize_ifreq(if_name);

Getting the Index of the Interface to Send a Packet

Khi cần gửi gói tin thông qua raw socket ở layer 2, ta cần biết index của network interface.

Sử dụng hàmioctl() (sys/ioctl.h) với macro SIOCGIFINDEX (bits/ioctls.h) để lấy index của network interface:

int get_if_index(int socket_descriptor, struct ifreq if_request)
{
    /* Get the interface index */
    if (ioctl(socket_descriptor, SIOCGIFINDEX, &if_request) < 0)
    {
        perror("Error: ioctl() failed");
        return -1;
    }
    return if_request.ifr_ifindex;
}

Getting the MAC Address of the Interface

Chúng ta cũng dùng hàm ioct() để lấy địa chỉ MAC của interface. Macro cần sử dụng là SIOCGIFHWADDR:

char *get_if_mac(int socket_descriptor, struct ifreq if_request)
{
    /* Get the MAC address */
    if (ioctl(socket_descriptor, SIOCGIFHWADDR, &if_request) < 0)
    {
        perror("Error: ioctl() failed");
        return NULL;
    }
    return mac_str(if_request.ifr_hwaddr.sa_data);
}

Thuộc tính sa_data được định nghĩa trong bits/socket.h là một mảng char 14 byte:

struct sockaddr
{
    __SOCKADDR_COMMON(sa_); /* Common data: address family and length.  */
    char sa_data[14];       /* Address data.  */
};

Để chuyển thành chuỗi địa chỉ MAC có dạng AA:BB:CC:DD:EE:FF thì ta cần xây dựng hàm mac_str() như sau:

char *mac_str(char *mac_addr)
{
    char *mac_str = malloc(18);
    sprintf(mac_str, "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x",
            (byte)mac_addr[0],
            (byte)mac_addr[1],
            (byte)mac_addr[2],
            (byte)mac_addr[3],
            (byte)mac_addr[4],
            (byte)mac_addr[5]);
    return mac_str;
}

Getting the IP Address of the Interface

Nếu muốn lấy địa chỉ IP của interface, ta dùng hàm ioctl() với macro SIOCGIFADDR:

char *get_if_ip(int socket_descriptor, struct ifreq if_request)
{
    if (ioctl(socket_descriptor, SIOCGIFADDR, &if_request) < 0)
    {
        perror("Error: ioctl() failed");
        return NULL;
    }
    return inet_ntoa(((struct sockaddr_in *)&if_request.ifr_addr)->sin_addr);
}

Tương tự với thuộc tính ifr_hwaddr, thuộc tính ifr_addr cũng có kiểu là sockaddr. Để chuyển mảng byte trong sockaddr thành chuỗi địa chỉ IP có dạng A.B.C.D thì ta cần dùng hàm inet_ntoa() (arpa/inet.h). Hàm này nhận vào đối số là struct in_addr (netinet/in.h):

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

Nên ta cần chuyển thuộc tính ifr_addr về kiểu sockaddr_in (netinet/in.h):

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
    __SOCKADDR_COMMON(sin_);
    in_port_t sin_port;      /* Port number.  */
    struct in_addr sin_addr; /* Internet address.  */
 
    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];
};

Rồi truyền thuộc tính sin_addr của sockaddr_in vào hàm inet_ntoa():

inet_ntoa(((struct sockaddr_in *)&if_request.ifr_addr)->sin_addr)

Constructing the ICMP Packet

Xây dựng hàm tạo gói tin ICMP (mà cụ thể là gói tin echo request) như sau:

Packet make_ping_packet(Packet payload)
{
    /* Create packet */
    Packet ping_packet = {
        .data = malloc(sizeof(struct icmphdr) + payload.len),
        .len = sizeof(struct icmphdr) + payload.len};
 
    /* Point to the ICMP header */
    struct icmphdr *icmph = (struct icmphdr *)ping_packet.data;
 
    /* Set ICMP header fields */
    icmph->type = ICMP_ECHO;            // ICMP echo request
    icmph->code = 0;                    // no code
    icmph->checksum = 0;                // set to 0 for now (will be calculated later)
    icmph->un.echo.id = htons(0);       // identifier
    icmph->un.echo.sequence = htons(0); // sequence number
 
    /* Copy packet payload */
    memcpy(&ping_packet.data[sizeof(struct icmphdr)], payload.data, payload.len);
 
    /* Calculate checksum */
    icmph->checksum = checksum(ping_packet.data, ping_packet.len);
 
    return ping_packet;
}

Với Packet là một cấu trúc dữ liệu lưu trữ nội dung và kích thước của gói tin:

typedef struct
{
    byte *data;
    size_t len;
} Packet;

payload truyền vào có thể có dạng như sau:

/* Create ICMP payload */
char *ping_payload = "Hello there!";
Packet payload = {
	.data = (byte *)ping_payload,
	.len = strlen(ping_payload)};

Quá trình xây dựng gói tin:

  • Tạo ra một instance của cấu trúc Packet đồng thời cấp phát vùng nhớ cho thuộc tính data. Kích thước của vùng nhớ được cấp phát cũng như là của gói tin là tổng kích thước các header của ICMP và kích thước của payload.
  • Trỏ con trỏ của struct icmphdr *icmph (netinet/ip_icmp.h) đến vùng nhớ đã được cấp phát để thiết lập dữ liệu cho gói tin:
    • Type là ICMP_ECHO (tương ứng với 8) cho biết rằng đây là gói tin echo request.
    • Ta không sử dụng code nên gán giá trị là 0.
    • Checksum tạm thời gán bằng 0 (sẽ tính sau).
    • Id và sequence number của gói tin có thể gán giá trị tùy ý. Ở đây ta gán bằng 0. Lý do mà sử dụng hàm htons vì hai trường này đều có kích thước là 2 byte và do đó cần sử dụng network byte order.
  • Sao chép dữ liệu trong payload vào các vùng nhớ kế tiếp.
  • Tính toán checksum rồi gán cho trường checksum.

Hàm tính checksum:

/*
Calculate checksum by summing up half-words - 2 bytes - 16 bits (1 word is 4 bytes) and taking the 1's complement
    @param: buff: pointer to the buffer
    @param: len: buffer length
    @return: checksum value (half-word - 2 bytes)
 
*/
unsigned short checksum(byte buff[], int len)
{
    unsigned int sum = 0; // checksum, iterator
 
    if (len < 1)
        return 0; // if buffer is empty, exit
 
    for (int i = 0; i < len - 1; i += 2)
        sum += *(word *)&buff[i]; // sum up half-words
 
    if (len & 1)
        sum += buff[len - 1]; // if buffer length is odd, add the last byte
 
    /*
        sum >> 16: shift right 16 bits to get the high order word
        sum & 0xffff: get the low order word
        +: add the high and low order words to wrap around into 16 bits
        ~: 1's complement
    */
    return ~((sum >> 16) + (sum & 0xffff));
}

Để tính checksum, ta cộng tất cả các cụm 2 byte (nửa word) trong gói tin lại rồi lấy bù 1. Nếu không tính đúng checksum thì bên nhận sẽ hủy bỏ gói tin.

Seealso

Tham khào thêm: How to Calculate IP Header Checksum (With an Example) (thegeekstuff.com)

Constructing the IP Header

Xây dựng hàm tạo gói tin IP:

Packet make_ip_packet(char *src, char *dst, byte proto, Packet payload)
{
    /* Create packet */
    Packet ip_packet = {
        .data = malloc(sizeof(struct iphdr) + payload.len),
        .len = sizeof(struct iphdr) + payload.len};
 
    /* Point to the IP header */
    struct iphdr *iph = (struct iphdr *)ip_packet.data;
 
    /* Set IP header fields */
    iph->version = 4;                                            // version
    iph->ihl = 5;                                                // IP header length (5 means 5 words, 1 word is 4 bytes, so it will be 5x4=20 bytes in total)
    iph->tos = 0;                                                // priority
    iph->tot_len = htons(ip_packet.len);                         // total length (includes header and data)
    iph->id = htons(9999);                                       // identification (anything is valid)
    iph->frag_off = 0;                                           // fragment offset (0  means no fragmentation)
    iph->ttl = 64;                                               // time to live
    iph->protocol = proto;                                       // protocol
    iph->check = 0;                                              // set to 0 for now (will be calculated later)
    iph->saddr = inet_addr(src);                                 // source IP address
    iph->daddr = inet_addr(dst);                                 // destination IP address
    iph->check = checksum(ip_packet.data, sizeof(struct iphdr)); // calculate checksum
 
    /* Copy packet payload */
    memcpy(&ip_packet.data[sizeof(struct iphdr)], payload.data, payload.len);
 
    return ip_packet;
}

Các bước xây dựng gói tin tương tự với ICMP. Có một số điểm cần chú ý:

  • Loại giao thức (proto) là giá trị của đối số truyền vào (nếu là ICMP thì truyền vào IPPROTO_ICMP, thuộc netinet/in.h).
  • Giá trị IP nguồn và giá trị IP đích có kiểu là in_addr_t (bản chất là kiểu unsigned int). Ta sẽ dùng hàm inet_aton() để chuyển từ chuỗi có dạng A.B.C.D thành số nguyên không dấu.
  • Checksum của gói tin IP chỉ bao gồm các header và không tính phần payload (sizeof(struct iphdr)).

Constructing the Ethernet Header

Xây dựng hàm tạo frame Ethernet như sau:

Packet make_ethernet_frame(char *src, char *dst, Packet payload)
{
    /* Create packet */
    Packet ethernet_frame = {
        .data = malloc(sizeof(struct ether_header) + payload.len),
        .len = sizeof(struct ether_header) + payload.len};
 
    /* Point to the Ethernet header */
    struct ether_header *eh = (struct ether_header *)ethernet_frame.data;
    eh->ether_type = htons(ETH_P_IP);                 // ethernet type
    mac_addr(eh->ether_shost, (unsigned short *)src); // source MAC address
    mac_addr(eh->ether_dhost, (unsigned short *)dst); // destination MAC address
 
    /* Copy frame payload */
    memcpy(&ethernet_frame.data[sizeof(struct ether_header)], payload.data, payload.len);
 
    return ethernet_frame;
}

Loại gói tin Ethernet là ETH_P_IP thuộc linux/if_ether.h (tương ứng với 0x0800). Do giá trị có 2 byte nên cần dùng htons().

Hàm mac_addr là một macro giúp lưu chuỗi địa chỉ MAC có dạng AA:BB:CC:DD:EE:FF vào một mảng byte:

#define mac_addr(a, mac) sscanf(mac, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5])

Các bước xây dựng tương tự với ICMP và IP.

Sending the Packet

Xây dựng hàm như sau để gửi gói tin thông qua raw socket ở layer 3:

int send_packet(int socket_descriptor, char *dst, Packet packet)
{
    /* Target's address information */
    struct sockaddr_in target_address = {
        /* Padding to make the structure the same size as struct sockaddr */
        .sin_zero = {0, 0, 0, 0, 0, 0, 0, 0},
        /* IPv4 */
        .sin_family = AF_INET,
        /* Ignore port number */
        .sin_port = 0,
        /* Destination address in network byte order */
        .sin_addr.s_addr = inet_addr(dst)};
 
    /*
    Send packet
        @param: socket_descriptor: socket file descriptor
        @param: packet.data: pointer to the packet data
        @param: packet.len: packet length
        @param: flags, 0 means no flags
        @param: (struct sockaddr *)&target_address: pointer to the target address
        @param: sizeof(target_address): size of the target address
    */
    if (sendto(socket_descriptor, packet.data, packet.len, 0, (struct sockaddr *)&target_address, sizeof(target_address)) < 0)
    {
        perror("Error: sendto() failed");
        return -1;
    }
    else
    {
        printf("Success: spoofed IP packet sent\n");
    }
 
    return 0;
}

Biến sockaddr_in target_address lưu thông tin địa chỉ của bên nhận, với:

  • .sin_family có giá trị là AF_INET, giống với họ socket truyền vào hàm socket().
  • .sin_addr.s_addr lưu địa chỉ IP của bên nhận.

Hàm sendto() có 6 đối số:

  • Socket descriptor.
  • Nội dung của gói tin.
  • Kích thước của gói tin.
  • Các flag. Giá trị 0 tức là ta không dùng flag nào.
  • Thông tin địa chỉ của bên nhận có kiểu là struct sockaddr *.
  • Kích thước của cấu trúc lưu thông tin địa chỉ của bên nhận.

Hàm này trả về -1 nếu không gửi được gói tin.

Xây dựng và gửi gói tin như sau:

int main()
{
    char *if_name = "br-88ab64b587ba";
    char *dst = "10.9.0.5";
    char *ping_payload = "Hello there!";
 
    /* Open layer-3 raw socket */
    int layer3_socket_descriptor = open_raw_socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
 
    /* Open layer-2 raw socket*/
    int layer2_socket_descriptor = open_raw_socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
 
    /* Get layer 2 interface information */
    struct ifreq if_req = initialize_ifreq(if_name);
    char *src = get_if_ip(layer2_socket_descriptor, if_req);
    printf("Source IP address: %s\n", src);
 
    /* Create ICMP payload */
    Packet payload = {
        .data = (byte *)ping_payload,
        .len = strlen(ping_payload)};
 
    /* Create ping packet */
    Packet ping_packet = make_ping_packet(payload);
 
    /* Create ip packet */
    Packet ip_packet = make_ip_packet(src, dst, IPPROTO_ICMP, ping_packet);
    print_buff(ip_packet);
 
    /* Send packet */
    send_packet(layer3_socket_descriptor, dst, ip_packet);
}

Biên dịch và chạy:

root@docker-desktop:/volumes# ./icmp_spoofer
Source IP address: 10.9.0.1
Packet data: 45 00 00 28 27 0f 00 00 40 01 3f af 0a 09 00 01 0a 09 00 05 08 00 95 11 00 00 00 00 48 65 6c 6c 6f 20 74 68 65 72 65 21 
Packet length: 40
Success: spoofed IP packet sent

Sending the Frame

Hàm gửi frame thông qua raw socket ở layer 2:

int send_frame(int if_index, int socket_descriptor, char *dst, Packet frame)
{
    /* Low-level target's address information */
    struct sockaddr_ll target_address = {
        .sll_ifindex = if_index,
        .sll_halen = ETH_ALEN,
    };
    mac_addr(target_address.sll_addr, dst);
 
    /* Send frame */
    if (sendto(socket_descriptor, frame.data, frame.len, 0, (struct sockeaddr *)&target_address, sizeof(target_address)) < 0)
    {
        perror("Error: sendto() failed");
        return -1;
    }
    else
    {
        printf("Success: spoofed Ethernet frame sent\n");
    }
 
    return 0;
}

Tương tự với hàm send_packet() nhưng ta dùng cấu trúc sockaddr_ll để lưu thông tin địa chỉ của bên nhận thay vì sockaddr_in. Ta cần thiết lập 2 thuộc tính sau:

  • .sll_ifindex: index của interface gửi gói tin.
  • .sll_halen: độ dài của Ethernet header.
  • .sll_addr: địa chỉ MAC của bên nhận.

Xây dựng frame và gửi như sau:

int main()
{
    char *if_name = "br-88ab64b587ba";
    char *dst = "10.9.0.5";
    char *dst_mac = "02:42:0a:09:00:05";
    char *ping_payload = "Hello there!";
 
    /* Open layer-2 raw socket*/
    int layer2_socket_descriptor = open_raw_socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
 
    /* Get layer 2 interface information */
    struct ifreq if_req = initialize_ifreq(if_name);
    int if_index = get_if_index(layer2_socket_descriptor, if_req);
    printf("Source interface index: %d\n", if_index);
    char *src_mac = get_if_mac(layer2_socket_descriptor, if_req);
    printf("Source MAC address: %s\n", src_mac);
    char *src = get_if_ip(layer2_socket_descriptor, if_req);
    printf("Source IP address: %s\n", src);
 
    /* Create ICMP payload */
    Packet payload = {
        .data = (byte *)ping_payload,
        .len = strlen(ping_payload)};
 
    /* Create ping packet */
    Packet ping_packet = make_ping_packet(payload);
 
    /* Create ip packet */
    Packet ip_packet = make_ip_packet(src, dst, IPPROTO_ICMP, ping_packet);
    print_buff(ip_packet);
 
    /* Create ethernet frame */
    Packet ethernet_frame = make_ethernet_frame(src_mac, dst_mac, ip_packet);
    print_buff(ethernet_frame);
 
    /* Send frame */
    send_frame(if_index, layer2_socket_descriptor, dst_mac, ethernet_frame);
}

Có thể thấy, ta bọc gói tin IP ở bên trong một Ethernet frame.

Biên dịch và chạy:

root@docker-desktop:/volumes# ./icmp_spoofer
Source interface index: 5
Source MAC address: 02:42:cc:5a:fd:37
Source IP address: 10.9.0.1
Packet data: 45 00 00 28 27 0f 00 00 40 01 3f af 0a 09 00 01 0a 09 00 05 08 00 95 11 00 00 00 00 48 65 6c 6c 6f 20 74 68 65 72 65 21 
Packet length: 40
Packet data: 02 42 0a 09 00 05 02 42 cc 5a fd 37 08 00 45 00 00 28 27 0f 00 00 40 01 3f af 0a 09 00 01 0a 09 00 05 08 00 95 11 00 00 00 00 48 65 6c 6c 6f 20 74 68 65 72 65 21 
Packet length: 54
Success: spoofed Ethernet frame sent
list
from outgoing([[Raw Socket Programming]])
sort file.ctime asc

Resources