数据包嗅探和伪造背景

网络接口卡

设备通过网络接口卡(network interface card, NIC,简称网卡)接入网络。每个网卡都有一个硬件地址,叫MAC地址。

广播型介质

网络中的设备均连接到一个共享的介质上。常用的本地通信网络、以太网和WIFI都是广播型介质。

内核处理普通数据包原理

当数据包在传播介质中流动时,网络中的每个网卡都能听到所有广播的数据帧(frame),这些数据帧会被复制到 网卡的内存中,网卡会检查数据帧头部的目的地址。如果目的地址和该网卡的MAC地址相匹配,那么该数据帧就会 通过直接存储器访问(direct memory access, DMA)的方式被复制到操作系统内核的缓存中,接着网卡会 以中断的方式告诉CPU它接收到了新的数据,然后CPU会将它们全部从缓存中复制到一个队列中,以便为新数据包 的到来腾出空间。根据协议规定,内核在处理队列中的数据包时会调用不同的回调函数。

混杂模式

promiscuous mode,嗅探程序要处理目标地址和自身MAC地址不匹配的数据帧,要把网卡设置成混杂模式。 一般是网络管理员诊断网络问题或者是黑客监听使用。有线网络使用。

监听模式

monitor mode,无线网卡是通过监听模式进行嗅探的。由于无线网络存在相邻设备干扰的问题,会严重影响网络的 性能,为了解决这个问题,wifi设备通过不同的信道传递数据,接入点将相邻设备用不同的信道连接起来,从而减少了 冲突带来的影响。wifi网卡的设计也做了相应的调整,可以在整个可用带宽和信道的分片上进行通信,由于这样的设计, 当网卡处于监听模式时,它只能捕捉所监听信道中的802.11数据帧。
这意味着,与以太网能监听所有数据帧不同,由于存在不同的信道,可能会错过一个网络中其他信道传输的信息。大多数 无线网卡都不支持监听模式,即使支持,默认也是禁用。

BSD数据包过滤器

当进行网络嗅探时,嗅探器经常只会对某些特定类型的数据包感兴趣,如TCP数据包或者DNS数据包。
系统可以将所有捕获到的数据包交给嗅探程序,嗅探程序会丢弃它不需要的数据包,但是这种处理方式效率低下,因为 把这些没用的数据包从内核传到嗅探程序是需要花费CPU时间的。

UNIX操作系统定义了BSD数据包过滤器(BSD packet filter, BPF),用于在底层实现数据包的过滤。BPF允许用户 空间的程序将一个过滤器和一个套接字进行绑定,其本质上是为了告知内核尽早丢弃不需要的数据包。
过滤器一般是首先使用布尔操作符编写的可读性较强的代码,随后该代码被编译成伪代码传递给BPF驱动。

经过编译的bsd代码如下: 编译过的bsd代码 这段BPF代码只过滤了22号端口,但可读性很差,再绑定socket:

struct sock_fprog bpf = {
    .len = ARRAY_SIZE(code);
    .filter = code,
};

setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));

与bpf一旦与socket绑定,当数据包到达内核时,回调函数就会被调用,用于判断该数据包是否应该过滤。通过过滤的数据 包被压入协议栈。

字节顺序

字节顺序和处理器架构有关,和几位操作系统无关
x86系列计算机都是小端序

小端字节顺序的计算机先写最低字节,大端是先写最高字节。

名字来由

来自《格列佛游记》,吃鸡蛋先敲大头还是小头。大端字节意味着先从大的字节开始保存。

网络字节序: 为了解决字节顺序不匹配的问题,IANA(Internet Assigned Numbers Authority,互联网 数字分配机构)定义了一种名为网络字节顺序的字节顺序,这就要求计算机在将多个字节数据写入数据包时使用这种字节 顺序,而不是操作系统的字节顺序。这种字节顺序和大端字节顺序是相同的。

字节序宏函数:为了方便网络字节序和本机字节序之间转换,提供了如下宏函数,从而使得代码有可移植性。

宏函数 描述
htons() 把无符号短整数从本机字节序转网络字节序
htonl() 把无符号整数从本机字节序转网络字节序
ntohs() 把无符号短整数从网络字节序转本机字节序
ntohl() 把无符号整数从网络字节序转本机字节序

校验和

每一个数据包中都有校验和字段。

文档出处

RFC 1071给出了IP、ICMP、TCP和UDP数据包头中校验和的算法。

Ip数据包不需提供校验和

IP数据包不需提供校验和,系统会计算IP数据包头中的校验和字段,其他数据包头需要开发者计算。

// checksum.c
unsigned short in_cksum(unsigned short *buf, int length)
{
    unsigned short *w = buf;
    int nleft = length;
    int sum = 0;
    unsigned short temp = 0;

    while (nleft > 1)
    {
        sum += *w++;
        nleft -= 2;
    }

    if (nleft == 1)
    {
        *(u_char *)(&temp) = *(u_char *)w;
        sum += temp;
    }

    // 对每个16比特进行二进制反码求和
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return (unsigned short)(~sum);
}

根据RFC 768文档和RFC 793文档,TCP和UDP的校验和是伪头部中每16比特的反码和,该伪头部中包含了IP头部、 TCP/UDP头部以及数据的信息。
如果需要的话,该校验和的末尾会填充8位0,使之成为两个字节的整数倍。
为了计算校验和,需要先创建一个伪头部,然后使用上述in_chksum()函数计算校验和。

// 伪TCP头部
struct pseudo_tcp
{
    unsigned saddr, daddr;
    unsigned char mbz;
    unsigned char ptcl;
    unsigned short tcpl;
    struct tcpheader tcp;
    char payload[PACKET_LEN];
};

unsigned short calculate_tcp_checksum(struct ipheader *ip)
{
    struct tcpheader *tcp = (struct tcpheader *)((u_char *)ip + sizeof(struct ipheader));
    int tcp_len = ntohs(ip->iph_len)->sizeof(struct ipheader);

    // 伪造TCP头部计算校验和
    struct pesudo_tcp p_tcp;
    memset(&p_tcp, 0x0, sizeof(struct pseudo_tcp));

    p_tcp.saddr = ip->iph_sourceip.s_addr;
    p_tcp.daddr = ip->iph_destip.s_addr;
    p_tcp.mbz = 0;
    p_tcp.ptcl = IPPROTO_TCP;
    p_tcp.tcpl = htons(tcp_len);
    memcpy(&p_tcp.tcp, tcp, tcp_len);

    return (unsigned short) in_cksum((unsigned short *)&p_tcp, tcp_len + 12);
}