黑客技能:网络攻防的艺术之TCP协议篇

1. 概述


网络攻击的主要内容包括系统安全攻防、网络安全攻防、物理攻击与社会工程学三部分: 系统安全攻防主要是利用软件安全漏洞进行攻击,网络安全攻防利用协议栈的安全漏洞(不局限于此), 物理攻击与社会工程学攻击主要是利用人的心理弱点、物理设计缺陷。


TCP(Transmission Control Protocol) 传输控制协议是TCP/IP协议栈的核心协议,它位于IP协议层之上,在网络上的两台计算机之间提供可靠的、有序的通信通道。许多应用比如浏览器、SSH、Telnet、Email等使用TCP进行通信。TCP协议处于为应用提供主机到主机通信服务的传输层。


一般我们讲TCP提供可靠的有连接服务,这个可靠包括三层含义:

  • 数据有序传输

  • 丢包重传机制

  • 流量控制机制


2. TCP协议的工作原理


我们通过一个简单的TCP client程序和TCP Server程序来展示TCP建立连接、数据传输、断开连接的过程。以下这两个程序中,为了能清晰说明程序的通信过程,不做容错处理,力求简单。工作当中这样的程序是不能正常工作的。


2.1 TCP Client 程序


#include <stdio.h>

#include <string.h>

#include <errno.h>

#include <sys/socket.h>

#include <netinet/ip.h>

 

#include <unistd.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#define SER_ADDR "127.0.0.1"

#define SER_PORT 9999

//#define SER_ADDR "172.16.28.98"

 

/* main function */

int main(int argc, char *argv[])

{

        /**

         * Step 1: 创建一个socket, 指定SOCK_STREAM参数代表基于TCP协议

         *   如果是UDP协议,则需要用SOCK_DGRAM

         */

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

 

        /**

         * Step 2: 设置目标主机IP地址和端口号

         *   IP+Port, 标识网络上某个主机的通信进程

         */

    struct sockaddr_in dest;

    memset(&dest, 0, sizeof(struct sockaddr_in));

    dest.sin_family = AF_INET;

    dest.sin_addr.s_addr = inet_addr(SER_ADDR);

    dest.sin_port = htons(SER_PORT);

 

        /**

         * Step 3: 连接服务器

          */

    if (connect(sockfd, (struct sockaddr *)&dest,

                    sizeof(struct sockaddr_in)) != 0){

                /* 此处SYN Flood攻击会用到 */

                fprintf(stdout, "Error for connect: %s ", strerror(errno));

                return 1;

        }

 

        /**

         * Step 4: Server发送数据

         */

 

        char *buffer1 = "Hello Server! ";

    char *buffer2 = "Hello Again! ";

    write(sockfd, buffer1, strlen(buffer1));

 

    write(sockfd, buffer2, strlen(buffer2));

 

        /**

         *  Step 5: 关闭连接

         */

    close(sockfd);

 

        return 0;

}


一个客户端程序大概如下几个步骤:

1. 创建一个socket,通过指定参数SOCK_STREAM,来标识基于TCP传输, 如果需要用UDP协议的话,则需要指定SOCK_DGRAM。 特别指出,如果需要通过原始套接字进行通讯,需要指定SOCK_RAW。

2. 指定地址信息, 在网络上,我们通过IP地址和Port来标识一个特定主机上的进程,此处填写Server端的IP地址和端号。 

3. 与Server建立连接, TCP是基于连接下协议,因此在数据传输之前,需要通过三次握手建立连接。 

4. 收发数据,一旦连接建立成功,C/S两端就可以通过write/send/sendto/sendmsg发送数据,可以通过read/recv/recvfrom/recvmsg/接收数据 

5. 关闭连接, 当数据收发完毕后,连接不再需要时,可以通过close断开连接。


2.2 TCP Server程序

下面我们编写一个TCP Server程序,还是老规矩,力求简单,不做容错处理。

#include <stdio.h>

#include <string.h>

#include <sys/socket.h>

#include <netinet/ip.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/socket.h>

 

#define SER_ADDR 9999

 

int main(int argc, char *argv[])

{

    int sockfd, newsockfd;

    struct sockaddr_in my_addr, client_addr;

    char buffer[100];

 

        /**

         * Step 1: 创建一个socket, 指定SOCK_STREAM代表TCP

         */

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

        /**

         *  Step 2:   绑定一个端口号

         */

    memset(&my_addr, 0, sizeof(struct sockaddr_in));

    my_addr.sin_family = AF_INET;

    my_addr.sin_port = htons(SER_ADDR);

    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in));

 

        /**

         * Step 3: 监听连接

         */

    listen(sockfd, 5);

 

        fprintf(stdout, "Serve listenning.... ");

        while(1){

                /**

                 * Step 4: Accept 一个连接请求

                 */

                socklen_t client_len = sizeof(client_addr);

                newsockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);

 

                /**

                 * Step 5: 从当前连接读取数据 */

                memset(buffer, 0, sizeof(buffer));

                int len = read(newsockfd, buffer, 100);

                printf("Received %d bytes: %s", len, buffer);

 

                /**

                 * Step 6: 关闭当前链接*/

                close(newsockfd);

        }

 

        /**

         * Step 7: 关闭套接字

         */

        close(sockfd);

 

    return 0;

}


一个Server程序的大致步骤为:

1. 创建一个socket

2. 绑定一个端口号

3. 开始监听

4. 接受一个连接请求。

5. 收发数据

6. 关闭连接请求

7. 关闭socket

其中 4、5、6可作为一个循环,多次响应连接请求。


2. 3 掀开数据传输的面纱

一旦连接建立, OS分别为Client端和Server端申请两个Buffer, 一个是SendBuffer,用于发送数据,一个是ReceiveBuffer,用于接收数据。 TCP协议是全双工的,两端都可以发送和接收数据。详细流程见下图: 


这里面有几个问题,我们此处并不会对TCP协议完整展开解释,选取和我们后续课程相关重点的描述一下:

1.TCP提供面向连接的服务, 连接的建立过程在下一章节中,重点描述。

2.数据有序传输:每个数据包编个序号,数据包到达主机可能错序,在传输层调整顺序后上传。

3.丢包重传机制:引入滑动窗口机制, 窗口内的数据如果没有接到应答ack, “超时”进行重传。

4.流量控制机制:引入滑动窗口机制后,接收端实时通知发送端当前自己接受窗口大小,从而约束发送端的发送,进行流量控制


2.4 TCP Header


TCP 部分一般称之为Segment, 数据段, (补充: 在应用层--消息、传输层--数据段Segment、网络层--包/分组 packet、 链路层--帧frame、 物理层bits),对于TCP header的格式如上图所述, 详细解读如下:

  • 源端口号和目的端口号, 各占16bits

  • 32位序号 (seq)

  • 32位应答序号(ack)

  • TCP header长度: 4位, 以4字节度量, 故Header最长为64字节

  • 保留: 6位

  • 标志位: 6位, 包括 SYN、FIN、ACK、RST、PSH、URG

  • 滑动窗口: 16位, 可用于流量控制

  • 校验: 16位

  • 紧急指针:16位, 当URG标志置位时, 此指针有效,用于带外数据

  • 选项:0~320bits, 以32bits为单位,TCP可以通过options携带一些补充数据


我们讲,TCP是面向连接的服务, 因此就存在建立连接, 断开连接等操作。后续我们选取针对连接建立过程和连接断开过程进行攻击方面的展示。


3. TCP SYN Flood 攻击


3.1 TCP 建立连接(三次握手)

我们说TCP提供面向连接的服务, 因此数据发送前需要先通过三次握手建立连接:

5.第一次握手: 首先客户端C(?)主动发起连接,发送SYN(连接请求标志), 以及序号SEQ=x(序号x随机生成)到服务器端S。

6.第二次握手: 服务器端S接受到SYN后, 向客户端C也发送SYN及ACK, 且ack=x+1, 以及序号Seq=y(序号y随机生成)。

7.第三次握手: 客户端接到SYN及ACK后, 核查ack是否为x+1, 若正确, 则客户端C发送ACK 且ack=y+1,至服务器端S。

8.服务器端S接收到ACK,核查ack是否为y+1。若正确,则连接正常建立。


三方握手建立连接的过程详细见下图



3.2 SYN Flooding 攻击

在探讨SYN攻击之前,我们先看看linux内核对SYN是怎么处理的: 1. Server接收到SYN连接请求。 内部维护一个队列(我们暂称之半连接队列,半连接并不准确), 发送ack及syn给Client端,等待Client端的ack应答,接收到则完成三次握手建立连接。 如果接收不到ack应答,则根据延时重传规则继续发送ack及syn给客户端。


利用上述特点。我们构造网络包,源地址随机构建,意味着当Server接到SYN包时,应答ack和syn时不会得到回应。在这种情况下, Server端,内核就会维持一个很大的队列来管理这些半连接。 当半连接足够多的时候,就会导致新来的正常连接请求得不到响应, 也就是所谓的DOS攻击。

详细见下图所示:



3.3 SYN Flood 攻击防护手段

  • tcp_max_syn_backlog: 半连接队列长度

  • tcp_synack_retries: syn+ack 的重传次数

  • tcp_syncookies : syn dookie


一般的防御措施就是就是减小SYN+ACK重传次数,增加半连接队列长度,启用syn cookie。不过在高强度攻击面前,调优 tcp_syn_retries 和 tcp_max_syn_backlog 并不能解决根本问题,更有效的防御手段是激活 tcp_syncookies,在连接真正创建起来之前,它并不会立刻给请求分配数据区存储连接状态,而是通过构建一个带签名的序号来屏蔽伪造请求。