From c690b16501e6899847de99a43edcb68c4d978b77 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 21 Feb 2021 14:46:26 +0100 Subject: [PATCH 1/5] Print basic RTP/STUN packet info --- xcloud/scripts/pcap_reader.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/xcloud/scripts/pcap_reader.py b/xcloud/scripts/pcap_reader.py index b69dbe1..e67aef9 100644 --- a/xcloud/scripts/pcap_reader.py +++ b/xcloud/scripts/pcap_reader.py @@ -2,13 +2,35 @@ PCAP Parser for XCloud network traffic """ import argparse +import logging import dpkt +from aiortc import rtp +from aioice import stun from construct.lib import containers +from ..protocol import packets + +logging.basicConfig(level=logging.DEBUG) containers.setGlobalPrintFullStrings(True) +LOG = logging.getLogger(__name__) + +def print_stun(stun: stun.Message) -> None: + print(f'STUN: {stun}') + +def print_rtp(rtp: rtp.RtpPacket) -> None: + try: + payload_name = packets.PayloadType(rtp.payload_type) + except: + payload_name = '' + print(f'RTP: {payload_name.name} {rtp}') + +PACKET_TYPES = [ + (stun.parse_message, print_stun), + (rtp.RtpPacket.parse, print_rtp) +] def packet_filter(filepath): with open(filepath, 'rb') as fh: @@ -28,8 +50,14 @@ def packet_filter(filepath): def parse_file(pcap_filepath: str) -> None: for packet, timestamp in packet_filter(pcap_filepath): - print(packet, timestamp) - + packet = bytes(packet.data) + for cls, print_func in PACKET_TYPES: + try: + instance = cls(packet) + print_func(instance) + except: + pass + def main(): parser = argparse.ArgumentParser( From a427feb5b3b6770ee61947afee225ff9533daed5 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 21 Feb 2021 16:24:50 +0100 Subject: [PATCH 2/5] Teredo: Basic parsing of teredo IPv6 packet header --- xcloud/protocol/ipv6.py | 26 ++++++++++++++++++++++++++ xcloud/protocol/teredo.py | 31 +++++++++++++++++++++++++++++++ xcloud/scripts/pcap_reader.py | 9 +++++++-- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 xcloud/protocol/ipv6.py create mode 100644 xcloud/protocol/teredo.py diff --git a/xcloud/protocol/ipv6.py b/xcloud/protocol/ipv6.py new file mode 100644 index 0000000..aea6bbd --- /dev/null +++ b/xcloud/protocol/ipv6.py @@ -0,0 +1,26 @@ +import dpkt + +class IPv6Packet: + def __init__( + self, + ipv6_base: dpkt.ip6.IP6 + ) -> None: + self.ipv6_base = ipv6_base + + @property + def data(self) -> bytes: + return self.ipv6_base.data + + def __repr__(self): + return ( + f"IPv6Packet(V={self.ipv6_base.v}, SRC={self.ipv6_base.src}, DST={self.ipv6_base.dst}, PLEN={self.ipv6_base.plen} NEXT={self.ipv6_base.nxt} HLIM={self.ipv6_base.hlim}) DATA={self.data}" + ) + + @classmethod + def parse(cls, data: bytes): + ipv6_base = dpkt.ip6.IP6(data) + if ipv6_base.v != 6: + raise ValueError( + f'Invalid IP version: Not 6' + ) + return cls(ipv6_base) \ No newline at end of file diff --git a/xcloud/protocol/teredo.py b/xcloud/protocol/teredo.py new file mode 100644 index 0000000..e161138 --- /dev/null +++ b/xcloud/protocol/teredo.py @@ -0,0 +1,31 @@ +import os +from dataclasses import dataclass, field +from struct import pack, unpack, unpack_from +from typing import Any, List, Optional, Tuple, Union + +from . import ipv6 + +TEREDO_PORT = 3544 +TEREDO_HEADER_LENGTH = 22 + +class TeredoPacket: + def __init__( + self, + ipv6_base: ipv6.IPv6Packet + ) -> None: + self.ipv6 = ipv6_base + + def __repr__(self) -> str: + return ( + f"TeredoPacket(IPv6={self.ipv6})" + ) + + @classmethod + def parse(cls, data: bytes): + if len(data) < TEREDO_HEADER_LENGTH: + raise ValueError( + f"Teredo packet length is less than {TEREDO_HEADER_LENGTH} bytes" + ) + base = ipv6.IPv6Packet.parse(data) + return cls(base) + diff --git a/xcloud/scripts/pcap_reader.py b/xcloud/scripts/pcap_reader.py index e67aef9..fe37bf9 100644 --- a/xcloud/scripts/pcap_reader.py +++ b/xcloud/scripts/pcap_reader.py @@ -9,7 +9,7 @@ from aioice import stun from construct.lib import containers -from ..protocol import packets +from ..protocol import packets, teredo logging.basicConfig(level=logging.DEBUG) @@ -27,9 +27,13 @@ def print_rtp(rtp: rtp.RtpPacket) -> None: print(f'RTP: {payload_name.name} {rtp}') +def print_teredo(teredo: teredo.TeredoPacket) -> None: + print(f'TEREDO: {teredo}') + PACKET_TYPES = [ (stun.parse_message, print_stun), - (rtp.RtpPacket.parse, print_rtp) + (rtp.RtpPacket.parse, print_rtp), + (teredo.TeredoPacket.parse, print_teredo) ] def packet_filter(filepath): @@ -56,6 +60,7 @@ def parse_file(pcap_filepath: str) -> None: instance = cls(packet) print_func(instance) except: + #LOG.exception('Failed to parse') pass From 56600749dd3a6be9ffa64bfe73b7b9e793e7967d Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 21 Feb 2021 19:24:52 +0100 Subject: [PATCH 3/5] Teredo: Parse teredo data from IPv6 packet --- tests/conftest.py | 30 +++++++++++++++++++- tests/test_teredo.py | 29 +++++++++++++++++++ xcloud/protocol/ipv6.py | 34 +++++++++++++++++++++- xcloud/protocol/teredo.py | 60 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 tests/test_teredo.py diff --git a/tests/conftest.py b/tests/conftest.py index 0d71f77..d7b7cec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from typing import Dict import os import pytest +from binascii import unhexlify @pytest.fixture(scope='session') def test_data() -> Dict[str, bytes]: @@ -10,4 +11,31 @@ def test_data() -> Dict[str, bytes]: with open(os.path.join(data_path, f), 'rb') as fh: data[f] = fh.read() - return data \ No newline at end of file + return data + +@pytest.fixture(scope='session') +def teredo_packet() -> bytes: + """ + Teredo IPv6 over UDP tunneling + Internet Protocol Version 6, Src: 2001:0:338c:24f4:1c38:f3fd:d2f3:c93d, Dst: 2001:0:338c:24f4:43b:30e3:d2f3:c93d + 0110 .... = Version: 6 + .... 0000 0000 .... .... .... .... .... = Traffic Class: 0x00 (DSCP: CS0, ECN: Not-ECT) + .... 0000 00.. .... .... .... .... .... = Differentiated Services Codepoint: Default (0) + .... .... ..00 .... .... .... .... .... = Explicit Congestion Notification: Not ECN-Capable Transport (0) + .... .... .... 0000 0000 0000 0000 0000 = Flow Label: 0x00000 + Payload Length: 0 + Next Header: No Next Header for IPv6 (59) + Hop Limit: 21 + Source Address: 2001:0:338c:24f4:1c38:f3fd:d2f3:c93d + Destination Address: 2001:0:338c:24f4:43b:30e3:d2f3:c93d + [Source Teredo Server IPv4: 51.140.36.244] + [Source Teredo Port: 3074] + [Source Teredo Client IPv4: 45.12.54.194] + [Destination Teredo Server IPv4: 51.140.36.244] + [Destination Teredo Port: 53020] + [Destination Teredo Client IPv4: 45.12.54.194] + """ + return unhexlify( + '6000000000003b1520010000338c24f41c38f3fdd2f3c93d20010000' + '338c24f4043b30e3d2f3c93d01049eb8960803080000c0a889db0c02' + ) \ No newline at end of file diff --git a/tests/test_teredo.py b/tests/test_teredo.py new file mode 100644 index 0000000..22d5fe9 --- /dev/null +++ b/tests/test_teredo.py @@ -0,0 +1,29 @@ +from binascii import unhexlify +from xcloud.protocol import teredo + +def test_teredo_convert(teredo_packet: bytes): + parsed = teredo.TeredoPacket.parse(teredo_packet) + + # Ipv6 + assert parsed.ipv6.version == 6 + assert parsed.ipv6.traffic_cls == 0x00 + assert parsed.ipv6.flow_label == 0x00 + assert parsed.ipv6.payload_len == 0x00 + assert parsed.ipv6.next_header == 59 # No next header + assert parsed.ipv6.hop_limit == 21 + assert parsed.ipv6.src == unhexlify('20010000338c24f41c38f3fdd2f3c93d') + assert parsed.ipv6.dst == unhexlify('20010000338c24f4043b30e3d2f3c93d') + + # Teredo source + assert parsed.src_teredo.udp_port == 3074 + assert parsed.src_teredo.flags == 0x1c38 + assert str(parsed.src_teredo.teredo_server_ipv4) == '51.140.36.244' + assert str(parsed.src_teredo.client_pub_ipv4) == '45.12.54.194' + + # Teredo destination + assert parsed.dst_teredo.udp_port == 53020 + assert parsed.dst_teredo.flags == 0x43b + assert str(parsed.dst_teredo.teredo_server_ipv4) == '51.140.36.244' + assert str(parsed.dst_teredo.client_pub_ipv4) == '45.12.54.194' + + diff --git a/xcloud/protocol/ipv6.py b/xcloud/protocol/ipv6.py index aea6bbd..1d3d8d4 100644 --- a/xcloud/protocol/ipv6.py +++ b/xcloud/protocol/ipv6.py @@ -7,13 +7,45 @@ def __init__( ) -> None: self.ipv6_base = ipv6_base + @property + def version(self) -> int: + return self.ipv6_base.v + + @property + def traffic_cls(self) -> int: + return self.ipv6_base.fc + + @property + def flow_label(self) -> int: + return self.ipv6_base.flow + + @property + def payload_len(self) -> int: + return self.ipv6_base.plen + + @property + def next_header(self) -> int: + return self.ipv6_base.nxt + + @property + def hop_limit(self) -> int: + return self.ipv6_base.hlim + + @property + def src(self) -> bytes: + return self.ipv6_base.src + + @property + def dst(self) -> bytes: + return self.ipv6_base.dst + @property def data(self) -> bytes: return self.ipv6_base.data def __repr__(self): return ( - f"IPv6Packet(V={self.ipv6_base.v}, SRC={self.ipv6_base.src}, DST={self.ipv6_base.dst}, PLEN={self.ipv6_base.plen} NEXT={self.ipv6_base.nxt} HLIM={self.ipv6_base.hlim}) DATA={self.data}" + f"IPv6Packet(V={self.version}, SRC={self.src}, DST={self.dst}, PLEN={self.payload_len} NEXT={self.next_header} HLIM={self.hop_limit}) DATA={self.data}" ) @classmethod diff --git a/xcloud/protocol/teredo.py b/xcloud/protocol/teredo.py index e161138..deac9f2 100644 --- a/xcloud/protocol/teredo.py +++ b/xcloud/protocol/teredo.py @@ -2,22 +2,74 @@ from dataclasses import dataclass, field from struct import pack, unpack, unpack_from from typing import Any, List, Optional, Tuple, Union +from ipaddress import IPv4Address, IPv6Address from . import ipv6 TEREDO_PORT = 3544 TEREDO_HEADER_LENGTH = 22 + +@dataclass +class TeredoEndpoint: + teredo_server_ipv4: IPv4Address + flags: int + udp_port: int + client_pub_ipv4: IPv4Address + + +def convert_teredo_addr_to_endpoint( + teredo_addr: bytes +) -> TeredoEndpoint: + """ + 0x00-0x04 Prefix // 32 bits + 0x04-0x08 Teredo server IPv4 // 32 bits + 0x08-0x0A Flags // 16 bits + 0x0A-0x0C UDP Port // 16 bits + 0x0C-0x10 Client public IPv4 // 32 bits + + Client IP address and UDP port are obfuscated / inverted + """ + addr = IPv6Address(teredo_addr) + teredo_tuple = addr.teredo + if not teredo_tuple: + raise ValueError('Not a teredo address') + + prefix, server_ipv4, flags, udp_port, client_ipv4 = \ + unpack('!IIHHI', teredo_addr) + + # Deobfuscate/invert client address and port + client_ipv4 ^= 0xFFFFFFFF + udp_port ^= 0xFFFF + + # Convert IP addresses to object + server_ipv4 = IPv4Address(server_ipv4) + client_ipv4 = IPv4Address(client_ipv4) + + assert server_ipv4 == teredo_tuple[0] + assert client_ipv4 == teredo_tuple[1] + + return TeredoEndpoint( + teredo_server_ipv4=server_ipv4, + flags=flags, + udp_port=udp_port, + client_pub_ipv4=client_ipv4 + ) + class TeredoPacket: def __init__( self, - ipv6_base: ipv6.IPv6Packet + ipv6_base: ipv6.IPv6Packet, + src_teredo: TeredoEndpoint, + dst_teredo: TeredoEndpoint ) -> None: self.ipv6 = ipv6_base + self.src_teredo = src_teredo + self.dst_teredo = dst_teredo def __repr__(self) -> str: return ( - f"TeredoPacket(IPv6={self.ipv6})" + f"TeredoPacket(IPv6={self.ipv6}, SRC={self.src_teredo}, DST={self.dst_teredo})" ) @classmethod @@ -27,5 +79,7 @@ def parse(cls, data: bytes): f"Teredo packet length is less than {TEREDO_HEADER_LENGTH} bytes" ) base = ipv6.IPv6Packet.parse(data) - return cls(base) + src_teredo = convert_teredo_addr_to_endpoint(base.src) + dst_teredo = convert_teredo_addr_to_endpoint(base.dst) + return cls(base, src_teredo, dst_teredo) From 8a537da3f66be0b3adea8e4241cafadd1d127eb5 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 21 Feb 2021 23:33:16 +0100 Subject: [PATCH 4/5] PCAP: Parse teredo-wrapped payloads --- xcloud/protocol/ipv6.py | 8 +++-- xcloud/protocol/teredo.py | 2 +- xcloud/scripts/pcap_reader.py | 61 +++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/xcloud/protocol/ipv6.py b/xcloud/protocol/ipv6.py index 1d3d8d4..61968f7 100644 --- a/xcloud/protocol/ipv6.py +++ b/xcloud/protocol/ipv6.py @@ -1,4 +1,8 @@ import dpkt +from typing import Any +from enum import Enum + +NO_NEXT_HEADER = 59 class IPv6Packet: def __init__( @@ -40,12 +44,12 @@ def dst(self) -> bytes: return self.ipv6_base.dst @property - def data(self) -> bytes: + def data(self) -> Any: return self.ipv6_base.data def __repr__(self): return ( - f"IPv6Packet(V={self.version}, SRC={self.src}, DST={self.dst}, PLEN={self.payload_len} NEXT={self.next_header} HLIM={self.hop_limit}) DATA={self.data}" + f"IPv6Packet(V={self.version}, SRC={self.src}, DST={self.dst}, PLEN={self.payload_len} NEXT={self.next_header} HLIM={self.hop_limit})" ) @classmethod diff --git a/xcloud/protocol/teredo.py b/xcloud/protocol/teredo.py index deac9f2..c5cb9a9 100644 --- a/xcloud/protocol/teredo.py +++ b/xcloud/protocol/teredo.py @@ -35,7 +35,7 @@ def convert_teredo_addr_to_endpoint( if not teredo_tuple: raise ValueError('Not a teredo address') - prefix, server_ipv4, flags, udp_port, client_ipv4 = \ + _, server_ipv4, flags, udp_port, client_ipv4 = \ unpack('!IIHHI', teredo_addr) # Deobfuscate/invert client address and port diff --git a/xcloud/scripts/pcap_reader.py b/xcloud/scripts/pcap_reader.py index fe37bf9..d2ccf6f 100644 --- a/xcloud/scripts/pcap_reader.py +++ b/xcloud/scripts/pcap_reader.py @@ -3,39 +3,63 @@ """ import argparse import logging +from typing import Any, Optional import dpkt from aiortc import rtp from aioice import stun from construct.lib import containers -from ..protocol import packets, teredo +from ..protocol import packets, teredo, ipv6 logging.basicConfig(level=logging.DEBUG) containers.setGlobalPrintFullStrings(True) LOG = logging.getLogger(__name__) -def print_stun(stun: stun.Message) -> None: - print(f'STUN: {stun}') +def get_info_stun(stun: stun.Message) -> None: + return f'STUN: {stun}' -def print_rtp(rtp: rtp.RtpPacket) -> None: +def get_info_rtp(rtp: rtp.RtpPacket) -> None: try: payload_name = packets.PayloadType(rtp.payload_type) except: payload_name = '' - print(f'RTP: {payload_name.name} {rtp}') + return f'RTP: {payload_name.name} {rtp}' + +def get_info_teredo(teredo: teredo.TeredoPacket) -> None: + info = f'TEREDO: {teredo}' + if teredo.ipv6.next_header != ipv6.NO_NEXT_HEADER: + data = teredo.ipv6.data + if type(data) == bytes: + raise ValueError(f'TEREDO contains unparsed-subpacket: {data}') + subpacket_info = get_info_general(data) + info += f'\n -> TEREDO-WRAPPED: {subpacket_info}' + return info -def print_teredo(teredo: teredo.TeredoPacket) -> None: - print(f'TEREDO: {teredo}') PACKET_TYPES = [ - (stun.parse_message, print_stun), - (rtp.RtpPacket.parse, print_rtp), - (teredo.TeredoPacket.parse, print_teredo) + (stun.parse_message, get_info_stun), + (rtp.RtpPacket.parse, get_info_rtp), + (teredo.TeredoPacket.parse, get_info_teredo) ] +def get_info_general(packet: Any) -> Optional[str]: + if isinstance(packet, dpkt.udp.UDP): + data = bytes(packet.data) + for cls, info_func in PACKET_TYPES: + try: + instance = cls(data) + info = info_func(instance) + return info + except: + pass + elif isinstance(packet, bytes): + return '' + else: + return '' + def packet_filter(filepath): with open(filepath, 'rb') as fh: for ts, buf in dpkt.pcap.Reader(fh): @@ -46,23 +70,18 @@ def packet_filter(filepath): continue ip = eth.data - if not isinstance(ip.data, dpkt.udp.UDP): + subpacket = ip.data + if not isinstance(subpacket, dpkt.udp.UDP): continue - yield(ip.data, ts) + yield(subpacket, ts) def parse_file(pcap_filepath: str) -> None: for packet, timestamp in packet_filter(pcap_filepath): - packet = bytes(packet.data) - for cls, print_func in PACKET_TYPES: - try: - instance = cls(packet) - print_func(instance) - except: - #LOG.exception('Failed to parse') - pass - + info = get_info_general(packet) + if info: + print(info) def main(): parser = argparse.ArgumentParser( From bdf92a11d5b177586c85db517f7f40bb4a318630 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 21 Feb 2021 23:37:27 +0100 Subject: [PATCH 5/5] PCAP: Print RTP SSRC --- xcloud/scripts/pcap_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcloud/scripts/pcap_reader.py b/xcloud/scripts/pcap_reader.py index d2ccf6f..14aad61 100644 --- a/xcloud/scripts/pcap_reader.py +++ b/xcloud/scripts/pcap_reader.py @@ -26,7 +26,7 @@ def get_info_rtp(rtp: rtp.RtpPacket) -> None: except: payload_name = '' - return f'RTP: {payload_name.name} {rtp}' + return f'RTP: {payload_name.name} {rtp} SSRC={rtp.ssrc}' def get_info_teredo(teredo: teredo.TeredoPacket) -> None: info = f'TEREDO: {teredo}'