1. 程式人生 > 其它 >簡易版TCP實現Http Chunk

簡易版TCP實現Http Chunk

最近半年個人工作,生活變動比較大,所以不太活躍,目前正在調整中~
為什麼實現簡易版使用者態TCP:

  1. 為了給我的資產監控新增使用者態tcp掃描功能,加快掃描速度,多快好省
  2. 實現構造畸報文的方式繞過網路裝置,滿足一些奇怪的需求。tcp屬於核心態,不會提供讓我們胡作非為的功能。

本篇文章主要分為如下幾個部分:

  1. tcp/ip資料包的構建
  2. 實現tcp的基礎以及http傳輸的原理
  3. 簡單介紹種繞過姿勢

當然,目前工具暫時不開源,因為還沒有完善,待後面完善後再開源。目的是可以無損代替python中的tcp模組

構建tcp ip 資料包

乙太網幀

既然我們決定實現使用者態的tcp,那麼我們需要構造tcp/ip的資料包。關於如何使用libpcap發包,請參考上一篇文章。
學過計算機網路的同學都知道,傳送一段網路報文,首先是乙太網首部,隨後緊跟ip報文,再是tcp或者udp等運輸層資料報文。最終才是資料,如圖

所以我們需要根據協議,從乙太網幀開始構建資料報文。在這裡需要使用python提供的struct模組,將python的資料型別轉換為bytes陣列。因為乙太網幀並不需要校驗和,所以構造相對簡單。
在這裡我們並不需要考慮VLAN(虛擬區域網),因為在我們的執行環境中,交換機都配置為Access模式,很少有配置為Trunk或者Hybrid模式。當然,如果有其他特殊需求,例如跨VLAN等,可以考慮在構造乙太網幀中新增vlan。

注意,乙太網幀並不提供校驗等功能。如果發包頻率過快,會導致上層裝置丟棄報文。在二十年前,icmp傳送源抑制報文,但是現在該報文已被廢除。所以masscan的發包速率不可過快。

既然我們決定從資料鏈路層構建報文,我們也需要處理arp請求。我們在接收到arp請求後,假如請求的是我們自己的協議地址,那麼我們需要構建arp相應。如果我們的使用者態tcp程式的ip地址與系統配置的ip地址相同,那麼可以忽略arp請求響應。

@classmethod
def unpack(cls, px):
    point = 0
    # 硬體地址型別,網路層協議型別,硬體地址長度,網路層協議地址長度
    # 所以我們目前不支援ipv6
    hadware_type, protocol_type, hardware_addr_len, protocol_addr_len = struct.unpack("!HHBB", px[point:point + 6])
    point += 6
    # ipv4 的arp請求
    if protocol_type == Ether_Protocol.IPV4:
        oper, = struct.unpack("!H", px[point:point + 2])
        point += 2
        
        sender_mac_addr, = struct.unpack(f"!{hardware_addr_len}s", px[point:point + hardware_addr_len])
        point += hardware_addr_len
        sender_proto_addr, = struct.unpack(f"!{protocol_addr_len}s", px[point:point + protocol_addr_len])
        point += protocol_addr_len
        
        target_mac_addr, = struct.unpack(f"!{hardware_addr_len}s", px[point:point + hardware_addr_len])
        point += hardware_addr_len
        target_proto_addr, = struct.unpack(f"!{protocol_addr_len}s", px[point:point + protocol_addr_len])
            point += protocol_addr_len

這時候有的同學會問,那豈不是我們只要接受到特定網絡卡mac地址的請求,我們也可以胡亂迴應。理論上來講是這樣,但是要具體分析物理層。如果物理層是WLAN的話,AP是不會給你的網絡卡傳送不屬於你mac地址的資料報文。所以mac地址儘量不要亂改。

ip資料包

ip資料包的格式如下

    """
    0                 1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |Version|  IHL  |Type of Service|          Total Length         |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |         Identification        |Flags|      Fragment Offset    |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  Time to Live |    Protocol   |         Header Checksum       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                       Source Address                          |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Destination Address                        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Options                    |    Padding    |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        IP報文格式
        1. 4位IP-version 4位IP頭長度 8位服務型別 16位報文總長度
        2. 16位識別符號 3位標記位 13位片偏移 暫時不關注此行
        3. 8位TTL 8位協議 16位頭部校驗和
        4. 32位源IP地址
        5. 32位目的IP地址
    """

在網路中,ip,tcp,udp的校驗和計算公式都一致,程式碼如下。

    def checksum(self, raw_packet):
        chksum = 0
        if raw_packet%2:
            # 說明長度是奇數,需要在末尾padding一個byte的0
            raw_packet += b'\x00'
        for i in range(0, len(raw_tcp), 2):
            chksum += int.from_bytes(raw_packet[i:i + 2], "big", signed=False)
        chksum = (chksum >> 16) + (chksum & 0xffff)

        chksum = chksum + (chksum >> 16)
        return ~chksum & 0xffff

最終程式碼如下

    def pack(self):
        chksum = 0
        raw = struct.pack("!BBHHH", self.version << 4 | self.ipv4_header, 0xc0, self.tol, self.identification,
                              self.flag << 13 | self.offset)
        raw += struct.pack("!BBHII", self.ttl, self.protocol, chksum, ip2int(self.src_ip), ip2int(self.dst_ip))
        chksum = self.checksum(raw)
        raw = struct.pack("!BBHHH", self.version << 4 | self.ipv4_header, 0xc0, self.tol, self.identification,
                              self.flag << 13 | self.offset)
        raw += struct.pack("!BBHII", self.ttl, self.protocol, chksum, ip2int(self.src_ip), ip2int(self.dst_ip))

        return raw

tcp資料包

包結構如下

最終程式碼如下

 def pack(self):
        """
        打包tcp
        :return:
        """
        chksum = 0
        raw_tcp = struct.pack('>HHLLBBHHH', self.src_port, self.dst_port, self.seq_num, self.ack_num, self.data_offset,
                              self.flag, self.win_size, chksum, self.urg_pointer)
        raw_tcp += self.data

        chksum = self.chksum(raw_tcp)
        return struct.pack('>HHLLBBHHH', self.src_port, self.dst_port, self.seq_num, self.ack_num, self.data_offset,
                           self.flag, self.win_size, chksum, self.urg_pointer) + self.data

當然,如果想更詳細地瞭解tcp的狀態機,請參考Embedded Xinu作業系統的原始碼,該原始碼簡單易懂,連結如下
https://github.com/xinu-os/xinu/blob/28a035ae86ba2cd38b7c07f4d35fe8115ad3078d/device/tcp/tcpRecv.c

TCP 分包bypass

在這裡主要介紹一下seq與ack以及幾種標誌位。
在建立好tcp連線後,我們就可以傳送資料了。這時候標誌位需要設定為ACK。seq序列號為上一次傳送資料包的seq + 上次傳送資料的長度。如下程式碼

tcp = TcpPkt(self.port_me, self.dst_port, self.seq_num, self.ack_num, TcpFlag.ACK)
tcp.data = data
self.eth_pkt.set_transport(tcp)
rawsock_send_ipv4(pcap, self.eth_pkt.pack())
self.seq_num += len(data)

對於http這種協議,首先發送http請求頭,在請求頭中註明請求體的長度,也就是content-length。傳送完http請求頭後,在最後一條tcp報文中需要設定tcp ACK和PSH。PSH標誌位告訴上層應用可以接受訊息了。
當然對於http chunk這種編碼另說。這時候上層應用再根據content-length標註的長度繼續接收報文。

在接收到tcp的報文,需要回復ACK,當然這個ACK報文可以不需要攜帶資料。並且seq也不需要+1。ack的長度為接收到報文的seq與接收報文的資料長度。

elif tcp_session.state == State.ESTABLISHED:
    if recv_tcp.transport.data:
        tcp_session.ack_num = recv_tcp.transport.seq_num + len(recv_tcp.transport.data)
        tcp_session.data += recv_tcp.transport.data
        tcp = TcpPkt(tcp_session.port_me, tcp_session.dst_port,
                     tcp_session.seq_num, tcp_session.ack_num, TcpFlag.ACK)

        tcp_session.eth_pkt.set_transport(tcp)
        rawsock_send_ipv4(pcap, tcp_session.eth_pkt.pack())
        if recv_tcp.transport.flag & TcpFlag.PSH:
            tcp_session.push = True

一般情況下,一條http請求或者http響應,都在一個包中。在上一節我們可知,每個包最大可以1420個位元組。這足夠容納很多內容了。

這也就是為什麼很多安全裝置不願重組包的原因

  1. 作業系統預設會將一次請求塞進一個tcp保重,這樣安全裝置只檢查每一個包即可完成攔截任務。這樣既節省了資源,又完成任務。這也就是http chunk可以繞過WAF的原因。
  2. 在高速報文的請求中,防火牆很難追蹤每一條tcp會話,硬體不允許。

那麼我們在發tcp包的時候,只需要控制每個包傳送的長度,分多次發,最後一個數據包傳送PSH&ACK即可。最終實現截圖

這個時候我們再加入亂序發包的功能,延遲發包的功能,就可以更方便地繞過安全裝置。安全裝置即使重組tcp回話,假如每個包都延遲到達,這個延遲時間剛好處於安全裝置重組TCP會話的等待延遲與系統重組的延遲時間之間,就可以達到繞過安全裝置的目的。

這時我們已經達到發包實現分塊傳輸,但是怎麼讓對方裝置的回包也實現分塊傳輸呢。這時候我們需要藉助tcp的window滑動視窗機制。
TCP使用“視窗”,意味著傳送方傳送一個或更多資料包,接收方就會響應一個或所有資料包。當接收方開始一個TCP連線時,自身會開啟一個接收快取區作為臨時儲存,之後再交給程式處理。
當接收方傳送一個ACK響應(即對收到資料的響應)時,接收方會告訴傳送者下一次我能接收多少資料,我們管這個叫視窗大小(window size)一般這個視窗大小就是接收方緩衝區的大小。

我們只需要將tcp的window設定的足夠小,就可以實現對端裝置響應的分塊,如圖

同樣,我們可以啟動延遲確認資料等構造畸形請求的方式以干擾安全裝置重組tcp會話的功能。

QNSM

QSNM是否進行流重組,以條件編譯確定__QNSM_STREAM_REASSEMBLE,預設配置中是不進行TCP流重組的
同一個流的TCP都會進行流重組,上下行都在一個快取佇列中,最大支援8個報文,且不考慮重疊部分
重組方法基於 hashmap + 雙向連結串列
TCP流快取刪除方式:1. 老化 2. 無需進一步解析 3. 命中規則
具體參考
https://zhuanlan.zhihu.com/p/393121010

當然繞過姿勢還很多,只要我們實現了自己的使用者態TCP,就可以胡作非為~