Hacker 絕技——TCP 伺服器與客戶端最詳細的分析!

0x00 前言

「網絡」一直以來都是黑客最熱衷的競技場。數據在網絡中肆意傳播:主機掃描、代碼注入、網絡嗅探、數據篡改重放、拒絕服務攻擊……黑客的功底越深厚,能做的就越多。

Python 作為一種解釋型腳本語言,自 1991 年問世以來,其簡潔、明確、可讀性強的語法深受黑客青睞,特別在網絡工具的編寫上,避免了繁瑣的底層語法,沒有對運行速度的高效要求,使得 Python 成為安全工作者的必備殺手鐧。

本文作為「Python 絕技」系列工具文章的開篇,先介紹因特網的核心協議 TCP ,再以 Python 的 socket 模塊為例介紹網絡套接字,最後給出 TCP 服務器與客戶端的 Python 腳本,並演示兩者之間的通信過程。

0x01 TCP 協議

TCP(Transmission Control Protocol,傳輸控制協議)是一種面向連接、可靠的、基於字節流的傳輸層通信協議。

TCP 協議的執行過程分為連接創建(Connection Establishment)數據傳送(Data Transfer)

連接終止(Connection Termination)三個階段,其中「連接創建」與「連接終止」分別是耳熟能詳的 TCP 協議三次握手(TCP Three-way Handshake)四次揮手(TCP Four-way Handshake),也是理解本文 TCP 服務器與客戶端通信過程的兩個核心階段。

為了能更好地理解下述過程,對 TCP 協議頭的關鍵區段做以下幾點說明:

  • 報文的功能在 TCP 協議頭的標記符(Flags)區段中定義,該區段位於第 104~111 比特位,共佔 8 比特,每個比特位對應一種功能,置 1 代表開啟,置 0 代表關閉。例如,SYN 報文的標記符為 00000010,ACK 報文的標記符為 00010000,ACK + SYN 報文的標記符為 00010010。
  • 報文的序列號在 TCP 協議頭的序列號(Sequence Number)區段中定義,該區段位於第 32~63 比特位,共佔 32 比特。例如,在「三次握手」過程中,初始序列號 seq
  • seq 由數據發送方隨機生成。
  • 報文的確認號在 TCP 協議頭的確認號(Acknowledgement Number)
    區段中定義,該區段位於第 64~95 比特位,共佔 32 比特。例如,在「三次握手」過程中,確認號 ack
  • ack 為前序接收報文的序列號加 1,代表下一次期望接收到的報文序列號。

連接創建(Connection Establishment)

所謂的「三次握手」,即 TCP 服務器與客戶端成功建立通信連接必經的三個步驟,共需通過三個報文完成。

一般而言,首先發送 SYN 報文的一方是客戶端,服務器則是監聽來自客戶端的建立連接請求。

Handshake Step 1

客戶端向服務器發送 SYN 報文(SYN=1

SYN=1)請求建立連接。

此時報文的初始序列號為 seq=x

seq=x,確認號為 ack=0

ack=0。發送完畢後,客戶端進入 SYN_SENT 狀態。

Handshake Step 2

服務器接收到客戶端的 SYN 報文後,發送 ACK + SYN 報文(ACK=1,SYN=1

ACK=1,SYN=1)確認客戶端的建立連接請求,並也向其發起建立連接請求。

此時報文的序列號為 seq=y

seq=y,確認號為 ack=x+1

ack=x+1。發送完畢後,服務器進入 SYN_RCVD 狀態。

Handshake Step 3

客戶端接收到服務器的 SYN 報文後,發送 ACK 報文(ACK=1

ACK=1)確認服務器的建立連接請求。

此時報文的序列號為 seq=x+1

seq=x+1,確認號為 ack=y+1

ack=y+1。發送完畢後,客戶端進入 ESTABLISHED 狀態;當服務器接收該報文後,也進入了 ESTABLISHED 狀態。

至此,「三次握手」過程全部結束,TCP 通信連接成功建立。

讀者可參照以下「三次握手」的示意圖進行理解:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

連接終止(Connection Termination)

所謂的「四次揮手」,即 TCP 服務器與客戶端完全終止通信連接必經的四個步驟,共需通過四個報文完成。

由於 TCP 通信連接是全雙工的,因此每個方向的連接可以單獨關閉,即可視為一對「二次揮手」,或一對單工連接。主動先發送 FIN 報文的一方,意味著想要關閉到另一方的通信連接,即在此方向上不再傳輸數據,但仍可以接收來自另一方傳輸過來的數據,直到另一方也發送 FIN 報文,雙方的通信連接才完全終止。

注意,首先發送 FIN 報文的一方,既可以是客戶端,也可以是服務器。下面以客戶端先發起關閉請求為例,對「四次揮手」的過程進行講解。

Handshake Step 1

當客戶端不再向服務器傳輸數據時,則向其發送 FIN 報文(FIN=1

FIN=1)請求關閉連接。

此時報文的初始序列號為 seq=u

seq=u,確認號為 ack=0

ack=0(若此報文中 ACK=1

ACK=1,則 ack

ack 的值與客戶端的前序接收報文有關)。發送完畢後,客戶端進入 FIN_WAIT_1 狀態。

Handshake Step 2

服務器接收到客戶端的 FIN 報文後,發送 ACK 報文(ACK=1

ACK=1)確認客戶端的關閉連接請求。

此時報文的序列號為 seq=v

seq=v,確認號為 ack=u+1

ack=u+1。發送完畢後,服務器進入 CLOSE_WAIT 狀態;當客戶端接收該報文後,進入 FIN_WAIT_2 狀態。

注意,此時 TCP 通信連接處於半關閉狀態,即客戶端不再向服務器傳輸數據,但仍可以接收服務器傳輸過來的數據。

Handshake Step 3

當服務器不再向客戶端傳輸數據時,則向其發送 FIN + ACK 報文(FIN=1,ACK=1

FIN=1,ACK=1)請求關閉連接。

此時報文的序列號為 seq=w

seq=w(若在半關閉狀態,服務器沒有向客戶端傳輸過數據,則 seq=v+1

seq=v+1 ),確認號為 ack=u+1

ack=u+1。發送完畢後,服務器進入 LAST_ACK 狀態。

Handshake Step 4

客戶端接收到服務器的 FIN + ACK 報文後,發送 ACK 報文(ACK=1

ACK=1)確認服務器的關閉連接請求。

此時報文的序列號為 seq=u+1

seq=u+1,確認號為 ack=w+1

ack=w+1。發送完畢後,客戶端進入 TIME_WAIT 狀態;當服務器接收該報文後,進入 CLOSED

狀態;當客戶端等待了 2MSL 後,仍沒接到服務器的響應,則認為服務器已正常關閉,自己也進入 CLOSED 狀態。

至此,「四次揮手」過程全部結束,TCP 通信連接成功關閉。

讀者可參照以下「四次揮手」的示意圖進行理解:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

0x02 Network Socket

Network Socket(網絡套接字)是計算機網絡中進程間通信的數據流端點,廣義上也代表操作系統提供的一種進程間通信機制。

進程間通信(Inter-Process Communication,IPC)的根本前提是能夠唯一標示每個進程。在本地主機的進程間通信中,可以用 PID(進程 ID)唯一標示每個進程,但 PID 只在本地唯一,在網絡中不同主機的 PID 則可能發生衝突,因此採用「IP 地址 + 傳輸層協議 + 端口號」的方式唯一標示網絡中的一個進程。

小貼士:網絡層的 IP 地址可以唯一標示主機,傳輸層的 TCP/UDP 協議和端口號可以唯一標示該主機的一個進程。注意,同一主機中 TCP 協議與 UDP 協議的可以使用相同的端口號。

所有支持網絡通信的編程語言都各自提供了一套 socket API,下面以 Python 3 為例,講解服務器與客戶端建立 TCP 通信連接的交互過程:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

腦海中先對上述過程產生一定印象後,更易於理解下面兩節 TCP 服務器與客戶端的 Python 實現。

0x03 TCP 服務器

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • Line 6:定義一個 tcplink() 函數,第一個 conn 參數為服務器與客戶端交互數據的套接字對象,第二個 addr 參數為客戶端的 IP 地址與端口號,用二元組 (host, port) 表示。
  • Line 8:連接成功後,向客戶端發送歡迎信息 b"Welcome!\n"。
  • Line 9:進入與客戶端交互數據的循環階段。
  • Line 10:向客戶端發送詢問信息 b"What's your name?"。
  • Line 11:接收客戶端發來的 bytes 對象。
  • Line 12:若 bytes 對象為 b"exit",則向客戶端發送結束響應信息 b"Good bye!\n",並結束與客戶端交互數據的循環階段。
  • Line 15:若 bytes 對象不為 b"exit",則向客戶端發送問候響應信息 b"Hello %s!\n",其中 %s 是客戶端發來的 bytes 對象。
  • Line 16:關閉套接字,不再向客戶端發送數據。
  • Line 19:創建 socket 對象,第一個參數為 socket.AF_INET,代表採用 IPv4 協議用於網絡通信,第二個參數為 socket.SOCK_STREAM,代表採用 TCP 協議用於面向連接的網絡通信。
  • Line 20:向 socket 對象綁定服務器主機地址 (“127.0.0.1”, 6000),即本地主機的 TCP 6000 端口。
  • Line 21:開啟 socket 對象的監聽功能,等待客戶端的連接請求。
  • Line 24:進入監聽客戶端連接請求的循環階段。
  • Line 25:接收客戶端的連接請求,並獲得與客戶端交互數據的套接字對象 conn 與客戶端的 IP 地址與端口號 addr,其中 addr 為二元組 (host, port)。
  • Line 26:利用多線程技術,為每個請求連接的 TCP 客戶端創建一個新線程,實現了一臺服務器同時與多臺客戶端進行通信的功能。
  • Line 27:開啟新線程的活動。

0x04 TCP 客戶端

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • Line 5:創建 socket 對象,第一個參數為 socket.AF_INET,代表採用 IPv4 協議用於網絡通信,第二個參數為 socket.SOCK_STREAM,代表採用 TCP 協議用於面向連接的網絡通信。
  • Line 6:向 (“127.0.0.1”, 6000) 主機發起連接請求,即本地主機的 TCP 6000 端口。
  • Line 7:連接成功後,接收服務器發來的歡迎信息 b"Welcome!\n",並轉換為字符串後打印輸出。
  • Line 9:創建一個非空字符串變量 data,並賦初值為 "client"(只要是非空字符串即可),用於判斷是否接收來自服務器發來的詢問信息 b"What's your name?"。
  • Line 10:進入與服務器交互數據的循環階段。
  • Line 11:當變量 data 非空時,則接收服務器發來的詢問信息。
  • Line 13:要求用戶輸入名字。
  • Line 14:當用戶的輸入為空時,則重新開始循環,要求用戶重新輸入。
  • Line 16:當用戶的輸入非空時,則將字符串轉換為 bytes 對象後發送至服務器。
  • Line 17:接收服務器的響應數據,並將響應的 bytes 對象轉換為字符串後打印輸出。
  • Line 18:當用戶的輸入為 "exit" 時,則終止與服務器交互數據的循環階段,即將關閉套接字。
  • Line 21:關閉套接字,不再向服務器發送數據。

0x05 TCP 進程間通信

將 TCP 服務器與客戶端的腳本分別命名為 tcp_server.py 與 tcp_client.py,然後存至桌面,筆者將在 Windows 10 系統下用 PowerShell 進行演示。

小貼士:讀者進行復現時,要確保本機已安裝 Python 3,注意筆者已將默認的啟動路徑名 python 改為了 python3。

單服務器 VS 單客戶端

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  1. 在其中一個 PowerShell 中運行命令 python3 ./tcp_server.py,服務器顯示 Waiting for connection...,並監聽本地主機的 TCP 6000 端口,進入等待連接狀態;
  2. 在另一個 PowerShell 中運行命令 python3 ./tcp_client.py,服務器顯示 Accept new connection from 127.0.0.1:42101,完成與本地主機的 TCP 42101 端口建立通信連接,並向客戶端發送歡迎信息與詢問信息,客戶端接收到信息後打印輸出;
  3. 若客戶端向服務器發送字符串 Alice 與 Bob,則收到服務器的問候響應信息;
  4. 若客戶端向服務器發送空字符串,則被要求重新輸入;
  5. 若客戶端向服務器發送字符串 exit,則收到服務器的結束響應信息;
  6. 客戶端與服務器之間的通信連接已關閉,服務器顯示 Connection from 127.0.0.1:42101 is closed,並繼續監聽客戶端的連接請求。

單服務器 VS 多客戶端

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  1. 在其中一個 PowerShell 中運行命令 python3 ./tcp_server.py,服務器顯示 Waiting for connection...,並監聽本地主機的 TCP 6000 端口,進入等待連接狀態;
  2. 在另三個 PowerShell 中分別運行命令 python3 ./tcp_client.py,服務器同時與本地主機的 TCP 42719、42721、42722 端口建立通信連接,並分別向客戶端發送歡迎信息與詢問信息,客戶端接收到信息後打印輸出;
  3. 三臺客戶端分別向服務器發送字符串 Client1、Client2、Client3,並收到服務器的問候響應信息;
  4. 所有客戶端分別向服務器發送字符串 exit,並收到服務器的結束響應信息;
  5. 所有客戶端與服務器之間的通信連接已關閉,服務器繼續監聽客戶端的連接請求。

0x06 Python API Reference

socket 模塊

本節介紹上述代碼中用到的內建模塊 socket,是 Python 網絡編程的核心模塊。

socket() 函數

socket() 函數用於創建網絡通信中的套接字對象。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • family 參數代表地址族(Address Family),默認值為 AF_INET,用於 IPv4 網絡通信,常用的還有 AF_INET6,用於 IPv6 網絡通信。family 參數的可選值取決於本機操作系統。
  • type 參數代表套接字的類型,默認值為 SOCK_STREAM,用於 TCP 協議(面向連接)的網絡通信,常用的還有 SOCK_DGRAM,用於 UDP 協議(無連接)的網絡通信。
  • proto 參數代表套接字的協議,默認值為 0,一般忽略該參數,除非 family 參數為 AF_CAN,則 proto 參數需設置為 CAN_RAW 或 CAN_BCM。
  • fileno 參數代表套接字的文件描述符,默認值為 None,若設置了該參數,則其他三個參數將會被忽略。

創建完套接字對象後,需使用對象的內置函數完成網絡通信過程。注意,以下函數原型中的「socket」是指 socket 對象,而不是上述的 socket 模塊。

bind() 函數

bind() 函數用於向套接字對象綁定 IP 地址與端口號。注意,套接字對象必須未被綁定,並且端口號未被佔用,否則會報錯。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • address 參數代表套接字要綁定的地址,其格式取決於套接字的 family 參數。若 family 參數為 AF_INET,則 address 參數表示為二元組 (host, port),其中 host 是用字符串表示的主機地址,port 是用整型表示的端口號。

listen() 函數

listen() 函數用於 TCP 服務器開啟套接字的監聽功能。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • backlog 可選參數代表套接字在拒絕新連接之前,操作系統可以掛起的最大連接數。backlog 參數一般設置為 5,若未設置,系統會為其自動設置一個合理的值。

connect() 函數

connect() 函數用於 TCP 客戶端向 TCP 服務器發起連接請求。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • address 參數代表套接字要連接的地址,其格式取決於套接字的 family 參數。若 family 參數為 AF_INET,則 address 參數表示為二元組 (host, port),其中 host 是用字符串表示的主機地址,port 是用整型表示的端口號。

accept() 函數

accept() 函數用於 TCP 服務器接受 TCP 客戶端的連接請求。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

accept() 函數的返回值是二元組 (conn, address),其中 conn 是服務器用來與客戶端交互數據的套接字對象,address 是客戶端的 IP 地址與端口號,用二元組 (host, port) 表示。

send() 函數

send() 函數用於向遠程套接字對象發送數據。注意,本機套接字必須與遠程套接字成功連接後才能使用該函數,否則會報錯。可見,send() 函數只能用於 TCP 進程間通信,而對於 UDP 進程間通信應該用 sendto() 函數。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • bytes 參數代表即將發送的 bytes 對象數據。例如,對於字符串 "hello world!" 而言,需要用 encode() 函數轉換為 bytes 對象 b"hello world!" 才能進行網絡傳輸。
  • flags 可選參數用於設置 send() 函數的特殊功能,默認值為 0,也可由一個或多個預定義值組成,用位或操作符 | 隔開。詳情可參考 Unix 函數手冊中的 send(2),flags 參數的常見取值有 MSG_OOB、MSG_EOR 、MSG_DONTROUTE等。

send() 函數的返回值是發送數據的字節數。

recv() 函數

recv() 函數用於從遠程套接字對象接收數據。注意,與 send() 函數不同,recv() 函數既可用於 TCP 進程間通信,也能用於 UDP 進程間通信。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

  • bufsize 參數代表套接字可接收數據的最大字節數。注意,為了使硬件設備與網絡傳輸更好地匹配,bufsize 參數的值最好設置為 2 的冪次方,例如 4096。
  • flags 可選參數用於設置 recv() 函數的特殊功能,默認值為 0,也可由一個或多個預定義值組成,用位或操作符 | 隔開。詳情可參考 Unix 函數手冊中的 recv(2),flags 參數的常見取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。

recv() 函數的返回值是接收到的 bytes 對象數據。例如,接收到 bytes 對象 b"hello world!",最好用 decode() 函數轉換為字符串 "hello world!" 再打印輸出。

close() 函數

close() 函數用於關閉本地套接字對象,釋放與該套接字連接的所有資源。

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

threading 模塊

本節介紹上述代碼中用到的內建模塊 threading,是 Python 多線程的核心模塊。

Thread() 類

Thread() 類可以創建線程對象,用於調用 start() 函數啟動新線程。類原型如下:

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

  • group 參數作為以後實現 ThreadGroup() 類的保留參數,目前默認值為 None。
  • target 參數代表線程被 run() 函數激活後調用的函數,默認值為 None,即沒有任何函數會被調用。
  • name 參數代表線程名,默認值為 None,則系統會自動為其命名,格式為「Thread-N」,N 是從 1 開始的十進制數。
  • args 參數代表 target 參數指向函數的普通參數,用元組(tuple)表示,默認值為空元組 ()。
  • kwargs 參數代表 target 參數指向函數的關鍵字參數,用字典(dict)表示,默認值為空字典 {}。
  • daemon 參數用於標示進程是否為守護進程。若設置為 True,則標示為守護進程;若設置為 False,則標示為非守護進程;若設置為 None,則繼承當前父線程的 daemon 參數值。

創建完線程對象後,需使用對象的內置函數控制多線程活動。

start() 函數

start() 函數用於開啟線程活動。函數原型如下:

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!

注意,每個線程對象只能調用一次 start() 函數,否則會導致 RuntimeError 錯誤。

0x07 總結

本文介紹了 TCP 協議與 socket 編程的基礎知識,再用 Python 3 實現並演示了 TCP 服務器與客戶端的通信過程,其中還運用了簡單的多線程技術,最後將腳本中涉及到的 Python API 做成了的參考索引,有助於理解實現過程。

筆者水平有限,若文中出現不足或錯誤之處,還望大家不吝相告,多多包涵,歡迎讀者前來交流技術,感謝閱讀。

Python_Hacker 絕技——TCP 服務器與客戶端最詳細的分析!


分享到:


相關文章: