设计和实现一个TCP协议半连接的端口扫描程序

网络 网络管理
整体来说对网络知识的基本功要求还是很高的。关于TCP/IP协议栈这些基础知识点的本文就不列举了。

某学生粉丝发来问题:

图片

这个题目一看就知道这位同学是网络安全相关专业。

很多粉丝以为彭老师知识搞驱动的,

但是其实作为一个拥有多篇网络协议专利的老鸟,

网络知识还是比较擅长的!

应用层套接字、组网、网卡驱动都有所涉猎,

目前还缺Linux内核协议栈这块没深入研究,后期会补上。

一、题目总结

题目要求是扫描所有TCP半连接的端口,需要实现的功能如下:

  1. 攻击方启动任务1,循环向指定 服务器端+端口 发送SYN数据包,(端口从0开始递增)
  2. 如果该服务器上有服务打开了这个端口,就会回复SYN+ACK,此时服务端进入SYN_RCVD状态,
  3. 攻击方启动任务2,扫描收到的所有SYN+ACK数据包,如果客户端收到SYN+ACK,那么说明服务器改端口打开,任务2就可以将所有打开的端口信息打印出来
  • 任务1使用socket API
  • 任务2使用pcap库

二、TCP基础知识点

解决这个问题必须掌握以下几个知识点:

  1. 什么是TCP
  2. TCP3次握手
  3. 什么是半连接
  4. TCP、IP协议头
  5. 如何使用Libpcap库
  6. 线程、进程

整体来说对网络知识的基本功要求还是很高的。关于TCP/IP协议栈这些基础知识点的本文就不列举了。

下面主要强化下这个题目涉及的TCP的知识点。

1.TCP

首先就是我们必须了解TCP协议头:

图片

  • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题
  • 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题
  • 控制位:ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接 SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定 FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段

与本题目相关的是最主要字段是控制位,控制位的操作最主要体现在3次握手和4次握手。

2. tcp三次握手

图片

开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态:

  • 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SENT 状态
  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态
  • 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输

图片

图片

图片

3. tcp四次挥手

图片

四次挥手过程:

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭

4.TCP状态

TCP协议状态迁移图如下:

图片

  • CLOSED:表示初始状态
  • LISTEN:表示服务器端的某个SOCKET处于监听状态,可以接受连接了
  • SYN_RCVD:表示接收到了SYN报文
  • SYN_SENT:表示客户端已发送SYN报文
  • ESTABLISHED:表示连接已经建立了
  • TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了
  • CLOSING:表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。如果双方几乎在同时* close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接
  • CLOSE_WAIT:表示在等待关闭

5. 半连接/全连接

TCP半连接及全连接状态,在服务器的性能分析中,起着重要的作用,它通常是反应服务端的处理能力

1)半连接队列(syn queue)

客户端发送SYN包,服务端收到后回复SYN+ACK后,服务端进入SYN_RCVD状态,这个时候的socket会放到半连接队列。

图片

2)全连接队列(accept queue)

当服务端收到客户端的ACK后,socket会从半连接队列移出到全连接队列。当调用accpet函数的时候,会从全连接队列的头部返回可用socket给用户进程。

全连接队列中存放的是已完成TCP三次握手的过程,等待被处理的连接,在客户端及服务端的状态均为 ESTABLISHED

三、 抓包举例

要想学好网络,抓包工具是必须掌握的。

下图是一口君通过抓包工具抓取的一个完整的 tcp 3次握手 + HTTP GET请求 + 4次握手 的完整通信数据包。

图片

https://www.bilibili.com/video/BV1xr4y1T7cT/?vd_source=07570058a62e0e8a6cf489efac35cfec

四、 socket

关于socket API内容,大家可以的参考下面这篇文章《​​socket到底是什么?​​》

五、libpcap

libpcap是一个网络数据包捕获函数库,功能非常强大,Linux下著名的tcpdump就是以它为基础的。

libpcap主要由两部分组成:网络分接头(network tap)和数据过滤器(packet filter)。

网络分接头从网络设备驱动程序中收集数据进行拷贝,过滤器决定是否接收该数据包。

libpcap利用BSD packet filter(BPF)算法对网卡接收到的链路层数据包进行过滤。

libpcap的包捕获机制就是在数据链路层加一个旁路处理。当一个数据包到达网络接口时,libpcap首先利用已经创建的套接字从链路层驱动程序中获得该数据包的拷贝,再通过Tap函数将数据包发给BPF过滤器。

BPF过滤器根据用户已经定义好的过滤规则对数据包进行逐一匹配,匹配成功则放入内核缓冲区,并传递给用户缓冲区,匹配失败则直接丢弃。

如果没有设置过滤规则,所有数据包都将放入内核缓冲区,并传递给用户层缓冲区。

1. libpcap安装

  1. 在线安装

sudo apt-get  install  libpcap-dev

这种适合有网络的朋友

如何无法安装尝试更新下源:

sudo apt-get update

  1. 离线编译安装

http://www.tcpdump.org/#latest-release

图片

然后解压
tar zxvf libpcap-1.10.3.tar.gz
cd libpcap-1.10.3
./configure
sudo make
sudo make install

2. Libpcap的抓包流程:

  1. 查找网络设备:目的是发现可用的网卡,实现的函数为pcap_lookupdev(),如果当前有多个网卡,函数就会返回一个网络设备名的指针列表。
  2. 打开网络设备:利用上一步中的返回值,可以决定使用哪个网卡,通过函数pcap_open_live()打开网卡,返回用于捕捉网络数据包的秒数字。
  3. 获得网络参数:这里是利用函数pcap_lookupnet(),可以获得指定网络设备的IP地址和子网掩码。
  4. 编译过滤策略:Lipcap的主要功能就是提供数据包的过滤,函数pcap_compile()来实现。
  5. 设置过滤器:在上一步的基础上利用pcap_setfilter()函数来设置。
  6. 利用回调函数,捕获数据包:函数pcap_loop()和pcap_dispatch()来抓去数据包,也可以利用函数pcap_next()和pcap_next_ex()来完成同样的工作。
  7. 关闭网络设备:pcap_close()函数关系设备,释放资源。

3. 数据结构说明:

struct pcap_pkthdr {
struct timeval ts; /* time stamp */
bpf_u_int32 caplen; /* 抓到的数据包实际长度 */
bpf_u_int32 len; /*数据包的长度 */
};

4. libcap库函数

关于libcap的详细讲解,后续会出文章,

本文只讲几个重要的函数。

  • 打开网络接口

//这个函数会返回指定接口的pcap_t类型指针,后面的所有操作都要使用这个指针。
pcap_t * pcap_open_live(const char * device, int snaplen, int promisc, int to_ms, char * errbuf)
device:网络接口字符串,可以直接使用硬编码,比如eth0。
snaplen:对于每个数据包,从开头要抓多少个字节,我们可以设置这个值来只抓每个数据包的头部,而不关心具体的内容。典型的以太网帧长度是1518字节,但其他的某些协议的数据包会更长一点,但任何一个协议的一个数据包长度都必然小于65535个字节。
promisc:指定是否打开混杂模式(Promiscuous Mode),0表示非混杂模式,任何其他值表示混合模式。如果要打开混杂模式,那么网卡必须也要打开混杂模式,可以使用如下的命令打开eth0混杂模式:ifconfig eth0
to_ms:抓包时长单位为毫秒,0标示一直等待。
errbuf: 输出参数,打开网络接口失败原因。

  • 打开离线的pcap文件

pcap_t * pcap_open_offline (const char *fname, char *errbuf)
fname :文件名称。
errbuf :打开失败的错误信息。

  • 抓包函数

int pcap_loop(pcap_t * p, int cnt, pcap_handler callback, u_char * user)
p: 打开的pcap_t类型指针。
cnt:一共抓多少个包,如果为负数就一直循环。
callback:回调函数指针
user:传递给回调函数的参数。
void callback(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
userarg:是pcap_loop的最后一个参数,当收到足够数量的包后pcap_loop会调用callback回调函数,同时将pcap_loop()的user参数传递给它
pkthdr: 抓到的报文头信息。
packet:收到的包的数据。

  • 过滤函数编译

int pcap_compile(pcap_t * p, struct bpf_program * fp, char * str, int optimize, bpf_u_int32 netmask)
//fp:这是一个传出参数,存放编译后的bpf
//str:过滤表达式
//optimize:是否需要优化过滤表达式
//metmask:简单设置为0即可

  • 设置过滤函数

int pcap_setfilter(pcap_t * p,  struct bpf_program * fp)
//参数fp就是pcap_compile()的第二个参数,存放编译后的bpf

  • 释放网络接口

void pcap_close(pcap_t * p)
//该函数用于关闭pcap_open_live()获取的pcap_t的网络接口对象并释放相关资源。

  • 打开网络包保存文件

pcap_dumper_t * pcap_dump_open (pcap_t *p, const char *fname)
//p:是我们已经打开的网络设备,从这个设备接收数据包。
// fname:是我们要写入的文件名,随便起。
//return: 如果出错,会返回NULL。可以借此检查这个文件有没有打开。

  • 将网络包写入文件

void pcap_dump (u_char *user, const struct pcap_pkthdr *h, const u_char *sp)
user:就是文件描述符dumpfp,只不过要做一下类型转换。
由于这个函数一般在pcap_loop()的函数指针所指向的packet_handler中使用,所以packet_handler中的user就是这里的user。
h:就是pkt_header

  • 网络包文件关闭

pcap_dump_close(pcap_dumper_t * t);

5. libcap过滤规则

一些过滤表达式的例子如下:

  • 只接收源ip地址是192.168.1.177的数据包

src host 192.168.1.177

  • 只接收tcp/udp的目的端口是80的数据包

dst port 80

  • 只接收不使用tcp协议的数据包

not tcp

  • 只接收SYN标志位置位且目标端口是22或23的数据包(tcp首部开始的第13个字节)

tcp[13] == 0x02 and (dst port 22 or dst port 23)

  • 只接收icmp的ping请求和ping响应的数据包

icmp[icmptype] == icmp-echoreply or icmp[icmptype] == icmp-echo

  • 只接收以太网mac地址是00:e0:09:c1:0e:82的数据包

ether dst 00:e0:09:c1:0e:82

只接收ip的ttl=5的数据包(ip首部开始的第8个字节)

ip[8] == 5

本例只抓取ip地址为本地IP的数据包,然后程序再对数据包协议头进行解析:

host 192.168.0.113

六、设计方案

实现原理:

atach、cap进程运行在ubuntu中,要攻击的目的终端可以使网络中任意设备,只需要能ping通即可。本例在windows上测试,采用桥接模式将ubuntu的网口和windows的网口桥接起来。

图片

atach进程主要功能:

  • 创建tcp套接字
  • 设置需要攻击的终端的ip+port,然后执行connect函数
  • connect成功,说明对方该端口可以使用
  • 修改port值,重复前面3个步骤

cap进程主要功能:

  • 通过eth0,抓取指定规则:host 192.168.0.116数据包
  • 解析出以太头、tcp头,ip头、tcp头,判断tcp头中sync+ack位为1的所有数据包
  • 打印出步骤2过滤出来的数据包

代码流程:

图片

七、测试

1. 环境:

windows ip:192.168.0.116
ubuntu ip:192.168.0.113

2. 文件:

peng@ubuntu:~/work/test/pcap$ ls
atach header.c libpcap-1.10.3.tar.gz cap.c
cap libpcap-1.10.3 atach.c protocol.h

其中atach是上攻击方,用于向指定ip发送sync包 cap 用于检测所有网卡收到的sync+ack数据包 程序运行在ubuntu中。

3. 启动网络调试助手

在windows上启动网络调试助手,

建立几个Tcp Server,端口号分别为55、56、57

图片

在这里插入图片描述

4. 启动程序

1)首先启动cap

peng@ubuntu:~/work/test/pcap$ sudo ./cap 192.168.0.116
found device: eth0
netaddr:0000a8c0
try to open device eth0
filter:host 192.168.0.116

2)启动攻击程序atach

需要新开启一个终端。

peng@ubuntu:~/work/test/pcap$ ./atach 192.168.0.116

5. 运行截图如下:

图片

右边log可见,列举出了所有可以访问的端口,包括55、56、57。

注意:那个单词atach故意少了一个t,否则编译不过去:

图片

大家可以试试你们的编译器,刑不刑!

八、代码

代码已经同步到gitee,地址如下:

https://gitee.com/yikoulinux/pcap.git

责任编辑:武晓燕 来源: 一口Linux
相关推荐

2009-09-28 11:05:29

Ruby端口扫描

2022-06-15 13:40:38

端口扫描工具开源工具

2020-12-14 10:32:28

Web安全工具多线程

2024-06-19 19:07:53

2023-10-16 18:51:04

Masscan网络安全

2013-01-10 09:19:53

2010-12-22 21:57:00

Angry IP Sc

2019-09-16 09:29:01

TCP全连接队列半连接队列

2010-09-17 09:40:16

2024-06-28 08:00:00

端口扫描安全

2021-01-10 08:14:01

Go语言TCP扫描器

2020-09-18 10:12:24

KotlinTCP网络协议

2021-05-31 08:54:30

RustScanRust端口扫描器

2023-02-09 08:47:48

TCP协议FTP

2015-12-14 16:03:45

LinuxUnix端口扫描

2009-01-15 09:52:00

2009-11-18 09:59:41

2010-09-13 15:55:43

制作端口扫描器

2010-07-07 10:45:22

TCP UDP协议

2015-04-24 11:03:53

点赞
收藏

51CTO技术栈公众号