簡易版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等運輸層數據報文。最終纔是數據,如圖
image.png
所以我們需要根據協議,從以太網幀開始構建數據報文。在這裏需要使用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數據包

包結構如下
image.png
最終代碼如下

 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即可。最終實現截圖
image.png

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

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

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

image.png

同樣,我們可以啓動延遲確認數據等構造畸形請求的方式以干擾安全設備重組tcp會話的功能。

QNSM

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

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章