設計和實現一個TCP協議半連接的端口掃描程序

2023.04.07

設計和實現一個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
  • 1.

四、 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
  • 1.

這種適合有網絡的朋友

如何無法安裝嘗試更新下源:

sudo apt-get update
  • 1.

  1. 離線編譯安裝

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

圖片

然后解压
tar zxvf libpcap-1.10.3.tar.gz  
cd libpcap-1.10.3
./configure
sudo make
sudo make install
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

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;    /*数据包的长度 */
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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: 输出参数,打开网络接口失败原因。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

  • 打開離線的pcap文件

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

  • 抓包函數

int pcap_loop(pcap_t * p, int cnt, pcap_handler callback, u_char * user)
p: 打开的pcap_t类型指针。
cnt:一共抓多少个包,如果为负数就一直循环。
callback:回调函数指针
user:传递给回调函数的参数。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
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:收到的包的数据。
  • 1.
  • 2.
  • 3.
  • 4.

  • 過濾函數編譯

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

  • 設置過濾函數

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

  • 釋放網絡接口

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

  • 打開網絡包保存文件

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

  • 將網絡包寫入文件

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
  • 1.
  • 2.
  • 3.
  • 4.

  • 網絡包文件關閉

pcap_dump_close(pcap_dumper_t * t);
  • 1.

5. libcap過濾規則

一些過濾表達式的例子如下:

  • 只接收源ip地址是192.168.1.177的數據包

src host 192.168.1.177
  • 1.

  • 只接收tcp/udp的目的端口是80的數據包

dst port 80
  • 1.

  • 只接收不使用tcp協議的數據包

not tcp
  • 1.

  • 只接收SYN標誌位置位且目標端口是22或23的數據包(tcp首部開始的第13個字節)

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

  • 只接收icmp的ping請求和ping響應的數據包

icmp[icmptype] == icmp-echoreply or icmp[icmptype] == icmp-echo
  • 1.

  • 只接收以太網mac地址是00:e0:09:c1:0e:82的數據包

ether dst 00:e0:09:c1:0e:82
  • 1.

只接收ip的ttl=5的數據包(ip首部開始的第8個字節)

ip[8] == 5
  • 1.

本例只抓取ip地址為本地IP的數據包,然後程序再對數據包協議頭進行解析:

host 192.168.0.113
  • 1.

六、設計方案

實現原理:

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
  • 1.
  • 2.

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
  • 1.
  • 2.
  • 3.

其中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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

2)啟動攻擊程序atach

需要新開啟一個終端。

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

5. 運行截圖如下:

圖片

右邊log可見,列舉出了所有可以訪問的端口,包括55、56、57。

注意:那個單詞atach故意少了一個t,否則編譯不過去:

圖片

大家可以試試你們的編譯器,刑不刑!

八、代碼

代碼已經同步到gitee,地址如下:

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