Skip to main content
  1. Posts/

算无遗策:socket 编程发送 RAW 数据包

·

实习的时候写的一小段代码。

要求是用 C/C++ 写发送 RAW 数据包的工具,需要支持 TCP 和 UDP。查了下资料发现比想象的要复杂,并且只发现了发送 ICMP RAW 数据包的样例,照着改了改。

大致需求 #

  • 数据包以 RAW 方式发送
  • 支持 TCP 和 UDP 协议
  • 发送地址由使用者指定
  • 源地址随机
  • 发送消息内容随机

一些固定的部分 #

头文件 raw.h 中存在一个固定的结构体和固定的函数:

extern int errno;

#pragma pack(1)
typedef struct PACKET_RAW_HEADER {
    uint32_t dwIP;
    uint16_t uPort;

    PACKET_RAW_HEADER() : dwIP(0), uPort(0) {}
} PacketRawHeader, * pPacketRawHeader;
#pragma pack()

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP);

之后的修改都必须基于这二者进行。

因此可以根据这些固定的部分先把主函数写好:

#include "raw.h"

int main(int argc, char *argv[]) {
    if (argc != 4) {
        printf("Usage: %s [ip] [port] [tcp|udp]\n", argv[0]);
        exit(-1);
    }
    PacketRawHeader *pPacketRawHeader = new PacketRawHeader();
    pPacketRawHeader->dwIP = inet_addr(argv[1]);
    pPacketRawHeader->uPort = htons(atoi(argv[2]));
    bool bTCP = true;
    if (!strcmp(argv[3], "udp")) {
        bTCP = false;
    }
    printf("%s\n", strerror(sendrawpacket(pPacketRawHeader, bTCP)));

    return 0;
}

随后在主要需要修改的 sendrawpacket 函数中处理中断,关闭 socket

int sockfd;

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    signal (SIGINT, interrupt_handler);
    signal (SIGTERM, interrupt_handler);
}

void interrupt_handler (int signum) {
    close(sockfd);
    free(clientaddr);
    exit(0);
}

主要逻辑 #

创建 socket #

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    //...
    if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) <0) {
        fprintf(stderr,"Error creating socket:%s\n", strerror(errno));
        return errno;
    }
    //...
}

根据要求,需要采用 SOCK_RAW + IPPROTO_RAW 的方式创建 socket。此时必须自己填充 IP 头部 以及 TCP/UDP 头部。

设置地址 #

设置目标地址,该地址由命令行参数指定:

struct sockaddr_in* clientaddr = NULL;

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    //...
    clientaddr = (struct sockaddr_in *)malloc(sizeof(struct sockaddr_in));
    if (clientaddr == NULL) {
        fprintf(stderr,"Error allocating memory:%s\n", strerror(errno));
        goto end;
    }

    clientaddr->sin_family = AF_INET;
    clientaddr->sin_port = pRawHeader->uPort;
    clientaddr->sin_addr.s_addr = pRawHeader->dwIP;
    //...
end:
    close(sockfd);
    return errno;
}

而源地址则由程序随机生成:

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    char buffer[BUF_LEN] = {0};
    char src_ip[20] = {0};
    uint16_t src_port;
    char *string_data = NULL;
    size_t hdr_size = (bTCP ? (THDR_SZ) : (UHDR_SZ));
    //...
    srand(time(NULL));
    string_data = (char *) (buffer + IPHDR_SZ + hdr_size);
    for (int i = 0; i < MSG_LEN; ++i) {
        string_data[i] = '0' + rand()%72;
    }
    string_data[MSG_LEN] = 0;
    printf("Message: %s\n", string_data);

    sprintf(src_ip,"%d.%d.%d.%d", rand()%256, rand()%256, rand()%256, rand()%256);
    src_port = rand()%65535 + 1;
    printf("Source IP: %s\nSource Port: %d\n", src_ip, src_port);
    //...
}

其中定义的宏如下:

#define BUF_LEN 1024
#define MSG_LEN 50
#define IPHDR_SZ sizeof(struct iphdr)
#define THDR_SZ sizeof(struct tcphdr)
#define UHDR_SZ sizeof(struct udphdr)

这里用 buffer 存放完整的报文,string_data 存放消息,报文结构实际上是:

 ------------------------------------------
| IP Header | TCP/UDP Header | string_data |
 ------------------------------------------
 \                                         /
  ---------------- buffer -----------------

填充 IP 头部 #

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    // ...
    struct iphdr *ip_hdr = (struct iphdr *)buffer;
    //...
    ip_hdr->ihl = 5;
    ip_hdr->version = 4;
    ip_hdr->tos = 0;
    ip_hdr->tot_len = IPHDR_SZ + hdr_size + strlen(string_data);
    ip_hdr->ttl = 64;
    ip_hdr->protocol = (bTCP ? IPPROTO_TCP : IPPROTO_UDP);
    ip_hdr->saddr = inet_addr(src_ip);
    ip_hdr->daddr = clientaddr->sin_addr.s_addr;
    ip_hdr->check = csum((unsigned short *)ip_hdr, ip_hdr->tot_len);
    //...
}

这里需要对 IP 头部计算校验和,计算方法和之后 TCP / UDP 头部校验和计算方法相同,每 16 bit 进行反码求和:

unsigned short csum(unsigned short *ptr, int nbytes) 
{
    register long sum;
    unsigned short oddbyte;
    register short answer;

    sum = 0;
    while (nbytes> 1) {
        sum += *ptr++;
        nbytes -= 2;
    }
    if (nbytes == 1) {
        oddbyte = 0;
        *((u_char*)&oddbyte) = *(u_char*)ptr;
        sum += oddbyte;
    }

    sum = (sum>>16) + (sum & 0xffff);
    sum = sum + (sum>>16);
    answer = (short)~sum;

    return (answer);
}

填充 TCP / UDP 头部 #

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    // ...
    struct tcphdr *tcp_hdr = (struct tcphdr *)(buffer + IPHDR_SZ);
    struct udphdr *udp_hdr = (struct udphdr *)(buffer + IPHDR_SZ);
    //...
    if (bTCP) {
        tcp_hdr->source = htons(src_port);
        tcp_hdr->dest = clientaddr->sin_port;
        tcp_hdr->doff = 5;
        tcp_hdr->window = htons(200);
        tcp_hdr->syn = 1;
    } else {
        udp_hdr->source = htons(src_port);
        udp_hdr->dest = clientaddr->sin_port;
        udp_hdr->len = htons(8 + strlen(string_data));
    }
    //...
}

这里协议头部中使用了一些常用的参数值,使用 TCP 协议时发送 SYN 包。

填充 TCP / UDP 伪头部 #

先定义伪头部结构体:

struct pseudo_iphdr {
    uint32_t source_ip_addr;
    uint32_t dest_ip_addr;
    uint8_t fixed;
    uint8_t protocol;
    uint16_t len;
};

#define PHDR_SZ sizeof(struct pseudo_iphdr)

随后填充伪头部:

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    // ...
    struct pseudo_iphdr csum_hdr;
    //...
    csum_hdr.source_ip_addr = ip_hdr->saddr;
    csum_hdr.dest_ip_addr = clientaddr->sin_addr.s_addr;
    csum_hdr.fixed = 0;
    csum_hdr.protocol = (bTCP ? IPPROTO_TCP : IPPROTO_UDP);
    csum_hdr.len = htons(hdr_size + strlen(string_data));
    //...
}

计算 TCP / UDP 校验和 #

填充好伪头部后,便可以计算校验和了。首先将需要校验的部分放进 csum_buffer 中,也就是伪头部 + 头部 + 数据。随后用 csum 函数计算校验和并填入相应字段:

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    // ...
    char *csum_buffer = NULL;
    size_t psize;
    //...
    psize = PHDR_SZ + hdr_size + strlen(string_data);
    csum_buffer = (char *)calloc(psize, sizeof(char));
    if (csum_buffer == NULL) {
        fprintf(stderr,"Error allocating memory:%s\n", strerror(errno));
        goto end1;
    }

    memcpy(csum_buffer, (char *)&csum_hdr, PHDR_SZ);
    memcpy(csum_buffer + PHDR_SZ, udp_hdr, hdr_size + strlen(string_data));

    if (bTCP) {
        tcp_hdr->check = csum((unsigned short *) csum_buffer, psize);
    } else {
        udp_hdr->check = csum((unsigned short *) csum_buffer, psize);
    }

    free (csum_buffer);
    csum_buffer = NULL;
    //...
end1:
    free(clientaddr);
    clientaddr = NULL;
end:
    close(sockfd);
    return errno;
}

此时报文大概是这样:

 ----------------------------------------------------
| ... | Pseudo Header | TCP/UDP Header | string_data |
 ----------------------------------------------------
 \                   /
  ---- IP Header ----
 \                                                   /
  ---------------------- buffer ---------------------

发送数据包 #

直接使用 sendto 指定地址为 clientaddr 即可。

int sendrawpacket(PacketRawHeader* pRawHeader, bool bTCP)
{
    //...
    if (sendto(sockfd, buffer, ip_hdr->tot_len, 0, (struct sockaddr *)clientaddr, sizeof(struct sockaddr_in)) <0) {
        fprintf(stderr,"Error sending message:%s\n", strerror(errno));
        goto end1;
    }
end1:
    free(clientaddr);
    clientaddr = NULL;
end:
    close(sockfd);
    return errno;
}

最后,可以使用 wireshark 进行测试,并打开校验和验证功能。