Adding a PDF manual document for users.
[pingpong.git] / packet-padding / timing_detection_vpn_padding.py
1 import argparse\r
2 import ipaddress\r
3 import socket\r
4 import unicodecsv as csv\r
5 \r
6 from scapy.all import *\r
7 \r
8 \r
9 def find_matches(pcap_file, router_wan_ip, sig_duration):\r
10     # Read all packets into memory (stored as a list).\r
11     # This is slow and consumes lots of memory.\r
12     # There are more efficient ways to read the pcap (which clear each packet from memory after it's been processed).\r
13     # However, to simplify the detection implementation we stick with the quick-and-dirty approach.\r
14     pkts = rdpcap(pcap_file)\r
15     # The potential signature matches, with the request packet as the first item of a tuple, and all possible reply\r
16     # packets as the second item.\r
17     matches = []\r
18     for idx, p in enumerate(pkts):\r
19         # Only consider IP traffic (note: this does not account for IPv6).\r
20         if not IP in p:\r
21             continue\r
22         # Note: IP addresses are apparently stored in string form (odd)\r
23         src = p[IP].src\r
24         dst = p[IP].dst\r
25         if src != router_wan_ip:\r
26             # We are trying to find matches for a simple [C->S, S->C] signature, so we want to first identify an\r
27             # outbound (router-to-cloud) packet and then subsequently find all potential reply packets (cloud-to-router)\r
28             # If this is a cloud-to-router packet, it is of no interest to us at this stage, so move on.\r
29             continue\r
30         # TODO should we exclude all multicasts+broadcasts? They wouldn't occur and/or be tunneled?\r
31         if ipaddress.ip_address(dst).is_multicast:\r
32             # Don't include multicast traffic originating from the router in the results.\r
33             continue\r
34         # Find the set of potential reply packets for this request.\r
35         replies = find_reply_pkts(router_wan_ip, pkts, idx, sig_duration)\r
36         # Store packet index alongside packet so that we can provide packet numbers for post analysis.\r
37         matches.append(((p, idx), replies))\r
38     return matches\r
39 \r
40 \r
41 def find_reply_pkts(router_wan_ip, pkts, request_pkt_idx, sig_duration):\r
42     request_pkt = pkts[request_pkt_idx]\r
43     idx = request_pkt_idx + 1\r
44     reply_pkts = []\r
45     while idx < len(pkts) and pkts[idx].time - request_pkt.time <= sig_duration:\r
46         pkt = pkts[idx]\r
47         if is_inbound_ip_pkt(pkt, router_wan_ip):\r
48             # Only count IP packets with router WAN IP as destination as potential replies to the request.\r
49             # Store packet index alongside packet so that we can provide packet numbers for post analysis.\r
50             reply_pkts.append((pkt, idx))\r
51         idx += 1\r
52     return reply_pkts\r
53 \r
54 \r
55 def is_inbound_ip_pkt(pkt, router_wan_ip):\r
56     return IP in pkt and pkt[IP].dst == router_wan_ip\r
57 \r
58 \r
59 def write_matches_to_csv(matches, csv_filename):\r
60     key_req_pkt = 'request_pkt'\r
61     key_reply_pkts = 'reply_pkts'\r
62     key_reply_pkts_count = 'number_of_reply_pkts'\r
63     columns = [key_req_pkt, key_reply_pkts, key_reply_pkts_count]\r
64     with open (csv_filename, 'wb') as csv_file:\r
65         writer = csv.DictWriter(csv_file, fieldnames=columns)\r
66         writer.writeheader()\r
67         for m in matches:\r
68             request_pkt = m[0][0]\r
69             request_pkt_idx = m[0][1]\r
70             # Wireshark packet numbers start from 1 (are not 0-based)\r
71             request_pkt_num = request_pkt_idx + 1\r
72             reply_pkts_numbers = []\r
73             for (reply_pkt, reply_pkt_idx) in m[1]:\r
74                 reply_pkt_num = reply_pkt_idx + 1\r
75                 reply_pkts_numbers.append(reply_pkt_num)\r
76             row = { key_req_pkt: request_pkt_num,\r
77                     key_reply_pkts: '; '.join(str(pkt_num) for pkt_num in reply_pkts_numbers),\r
78                     key_reply_pkts_count: len(reply_pkts_numbers) }\r
79             writer.writerow(row)\r
80 \r
81 \r
82 if __name__ == '__main__':\r
83     desc = 'Perform detection on traffic in a VPN tunnel where traffic is padded; ' + \\r
84            'i.e., the detection is entirely based on timing information and packet directions. ' + \\r
85             'NOTE: THIS CODE IS SIMPLIFIED AND ONLY WORKS FOR SIMPLE [Client-to-Server, Server-to-Client] TWO ' + \\r
86             'PACKET SIGNATURES.'\r
87     parser = argparse.ArgumentParser(description=desc)\r
88     parser.add_argument('pcap_file', help='Full path to the target pcap file (detection target trace).')\r
89     parser.add_argument('router_wan_ip', help='IP of WAN interface of the home router (in decimal format).')\r
90     h = 'Duration of the signature ' + \\r
91         '(max time between request and reply packet for the two packets to be considered a match). ' + \\r
92         'Unit: seconds (floating point number expected).'\r
93     parser.add_argument('signature_duration',\r
94                         help=h, type=float)\r
95     parser.add_argument('output_csv', help='Filename of CSV file where results are to be written.')\r
96     args = parser.parse_args()\r
97 \r
98     pcap_file = args.pcap_file\r
99     router_wan_ip = args.router_wan_ip\r
100     signature_duration = args.signature_duration\r
101     output_csv = args.output_csv\r
102     print('Parsed arguments:')\r
103     print(f'pcap_file={pcap_file}')\r
104     print(f'router_wan_ip={router_wan_ip}')\r
105     print(f'signature_duration={signature_duration}')\r
106     print(f'output_csv={output_csv}')\r
107 \r
108     events = find_matches(pcap_file, router_wan_ip, signature_duration)\r
109     # for e in events:\r
110     #     request_pkt = e[0][0]\r
111     #     request_pkt_num = e[0][1] + 1\r
112     #     for (reply_pkt, reply_pkt_num) in e[1]:\r
113     #         print(f"MATCH: Packet number {request_pkt_num} (request) with packet number {reply_pkt_num + 1} (reply).")\r
114     write_matches_to_csv(events, output_csv)\r