MembersMgmtCommAttack.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. from enum import Enum
  2. class MessageType(Enum):
  3. """
  4. Defines possible botnet message types
  5. """
  6. TIMEOUT = 3
  7. SALITY_NL_REQUEST = 101
  8. SALITY_NL_REPLY = 102
  9. SALITY_HELLO = 103
  10. SALITY_HELLO_REPLY = 104
  11. class Message():
  12. """
  13. Defines a compact message type that contains all necessary information.
  14. """
  15. def __init__(self, msg_id: int, src, dst, type_: MessageType, time: float, refer_msg_id: int=-1):
  16. """
  17. Constructs a message with the given parameters.
  18. :param msg_id: the ID of the message
  19. :param src: something identifiying the source, e.g. ID or configuration
  20. :param dst: something identifiying the destination, e.g. ID or configuration
  21. :param type_: the type of the message
  22. :param time: the timestamp of the message
  23. :param refer_msg_id: the ID this message is a request for or reply to. -1 if there is no related message.
  24. """
  25. self.msg_id = msg_id
  26. self.src = src
  27. self.dst = dst
  28. self.type = type_
  29. self.time = time
  30. self.refer_msg_id = refer_msg_id
  31. def __str__(self):
  32. str_ = "{0}. at {1}: {2}-->{3}, {4}, refer:{5}".format(self.msg_id, self.time, self.src, self.dst, self.type, self.refer_msg_id)
  33. return str_
  34. from random import randint, randrange, choice
  35. from collections import deque
  36. from scipy.stats import gamma
  37. from lea import Lea
  38. from datetime import datetime
  39. from Attack import BaseAttack
  40. from Attack.AttackParameters import Parameter as Param
  41. from Attack.AttackParameters import ParameterTypes
  42. from ID2TLib import FileUtils, PaddingGenerator
  43. from ID2TLib.PacketGenerator import PacketGenerator
  44. from ID2TLib.IPGenerator import IPGenerator
  45. from ID2TLib.PcapAddressOperations import PcapAddressOperations
  46. from ID2TLib.CommunicationProcessor import CommunicationProcessor
  47. from ID2TLib.MacAddressGenerator import MacAddressGenerator
  48. from ID2TLib.PortGenerator import gen_random_server_port
  49. class MembersMgmtCommAttack(BaseAttack.BaseAttack):
  50. def __init__(self):
  51. """
  52. Creates a new instance of the Membership Management Communication.
  53. """
  54. # Initialize communication
  55. super(MembersMgmtCommAttack, self).__init__("Membership Management Communication Attack (MembersMgmtCommAttack)",
  56. "Injects Membership Management Communication", "Botnet communication")
  57. # Define allowed parameters and their type
  58. self.supported_params = {
  59. # parameters regarding attack
  60. Param.INJECT_AT_TIMESTAMP: ParameterTypes.TYPE_FLOAT,
  61. Param.INJECT_AFTER_PACKET: ParameterTypes.TYPE_PACKET_POSITION,
  62. Param.PACKETS_PER_SECOND: ParameterTypes.TYPE_FLOAT,
  63. Param.PACKETS_LIMIT: ParameterTypes.TYPE_INTEGER_POSITIVE,
  64. Param.ATTACK_DURATION: ParameterTypes.TYPE_INTEGER_POSITIVE,
  65. # use num_attackers to specify number of communicating devices?
  66. Param.NUMBER_INITIATOR_BOTS: ParameterTypes.TYPE_INTEGER_POSITIVE,
  67. # input file containing botnet communication
  68. Param.FILE_CSV: ParameterTypes.TYPE_FILEPATH,
  69. Param.FILE_XML: ParameterTypes.TYPE_FILEPATH,
  70. # the percentage of IP reuse (if total and other is specified, percentages are multiplied)
  71. Param.IP_REUSE_TOTAL: ParameterTypes.TYPE_PERCENTAGE,
  72. Param.IP_REUSE_LOCAL: ParameterTypes.TYPE_PERCENTAGE,
  73. Param.IP_REUSE_EXTERNAL: ParameterTypes.TYPE_PERCENTAGE,
  74. # the user-selected padding to add to every packet
  75. Param.PACKET_PADDING: ParameterTypes.TYPE_PADDING
  76. }
  77. # create dict with MessageType values for fast name lookup
  78. self.msg_types = {}
  79. for msg_type in MessageType:
  80. self.msg_types[msg_type.value] = msg_type
  81. def init_params(self):
  82. """
  83. Initialize some parameters of this communication-attack using the user supplied command line parameters.
  84. The remaining parameters are implicitly set in the provided data file. Note: the timestamps in the file
  85. have to be sorted in ascending order
  86. :param statistics: Reference to a statistics object.
  87. """
  88. # set class constants
  89. self.DEFAULT_XML_PATH = "resources/MembersMgmtComm_example.xml"
  90. # probability for responder ID to be local if comm_type is mixed
  91. self.PROB_RESPND_IS_LOCAL = 0
  92. # PARAMETERS: initialize with default values
  93. # (values are overwritten if user specifies them)
  94. #self.add_param_value(Param.INJECT_AFTER_PACKET, randint(0, int(self.statistics.get_packet_count()/4)))
  95. self.add_param_value(Param.INJECT_AFTER_PACKET, 1)
  96. self.add_param_value(Param.PACKETS_PER_SECOND, 0)
  97. self.add_param_value(Param.FILE_XML, self.DEFAULT_XML_PATH)
  98. # Alternatively new attack parameter?
  99. duration = int(float(self._get_capture_duration()))
  100. # if duration == 0:
  101. # duration = 1
  102. self.add_param_value(Param.ATTACK_DURATION, duration)
  103. self.add_param_value(Param.NUMBER_INITIATOR_BOTS, 1)
  104. # default locality behavior
  105. # self.add_param_value(Param.COMM_TYPE, "mixed")
  106. # TODO: change 1 to something better
  107. self.add_param_value(Param.IP_REUSE_TOTAL, 1)
  108. self.add_param_value(Param.IP_REUSE_LOCAL, 0.5)
  109. self.add_param_value(Param.IP_REUSE_EXTERNAL, 0.5)
  110. # add default additional padding
  111. self.add_param_value(Param.PACKET_PADDING, 20)
  112. def generate_attack_pcap(self):
  113. # create the final messages that have to be sent, including all bot configurations
  114. messages = self._create_messages()
  115. if messages == []:
  116. return 0, []
  117. # for msg in messages:
  118. # print(msg)
  119. # Setup (initial) parameters for packet creation loop
  120. BUFFER_SIZE = 1000
  121. pkt_gen = PacketGenerator()
  122. file_timestamp_prv = messages[0].time
  123. pcap_timestamp = self.get_param_value(Param.INJECT_AT_TIMESTAMP)
  124. padding = self.get_param_value(Param.PACKET_PADDING)
  125. packets = deque(maxlen=BUFFER_SIZE)
  126. total_pkts = 0
  127. limit_packetcount = self.get_param_value(Param.PACKETS_LIMIT)
  128. limit_duration = self.get_param_value(Param.ATTACK_DURATION)
  129. duration = 0
  130. path_attack_pcap = None
  131. # create packets to write to PCAP file
  132. for msg in messages:
  133. # retrieve the source and destination configurations
  134. id_src, id_dst = msg.src["ID"], msg.dst["ID"]
  135. ip_src, ip_dst = msg.src["IP"], msg.dst["IP"]
  136. mac_src, mac_dst = msg.src["MAC"], msg.dst["MAC"]
  137. port_src, port_dst = msg.src["Port"], msg.dst["Port"]
  138. ttl = msg.src["TTL"]
  139. # update timestamps and duration
  140. file_timestamp = msg.time
  141. file_time_delta = file_timestamp - file_timestamp_prv
  142. pcap_timestamp += file_time_delta
  143. duration += file_time_delta
  144. file_timestamp_prv = file_timestamp
  145. # if total number of packets has been sent or the attack duration has been exceeded, stop
  146. if ((limit_packetcount is not None and total_pkts >= limit_packetcount) or
  147. (limit_duration is not None and duration >= limit_duration)):
  148. break
  149. # if the type of the message is a NL reply, determine the number of entries
  150. nl_size = 0
  151. if msg.type == MessageType.SALITY_NL_REPLY:
  152. nl_size = randint(1, 25) # what is max NL entries?
  153. # create suitable IP/UDP packet and add to packets list
  154. packet = pkt_gen.generate_mmcom_packet(ip_src=ip_src, ip_dst=ip_dst, ttl=ttl, mac_src=mac_src, mac_dst=mac_dst,
  155. port_src=port_src, port_dst=port_dst, message_type=msg.type, neighborlist_entries=nl_size)
  156. PaddingGenerator.add_padding(packet, padding,True, True)
  157. packet.time = pcap_timestamp
  158. packets.append(packet)
  159. total_pkts += 1
  160. # Store timestamp of first packet (for attack label)
  161. if total_pkts <= 1:
  162. self.attack_start_utime = packets[0].time
  163. elif total_pkts % BUFFER_SIZE == 0: # every 1000 packets write them to the PCAP file (append)
  164. packets = list(packets)
  165. PaddingGenerator.equal_length(packets, padding = padding)
  166. last_packet = packets[-1]
  167. path_attack_pcap = self.write_attack_pcap(packets, True, path_attack_pcap)
  168. packets = deque(maxlen=BUFFER_SIZE)
  169. # if there are unwritten packets remaining, write them to the PCAP file
  170. if len(packets) > 0:
  171. packets = list(packets)
  172. PaddingGenerator.equal_length(packets, padding = padding)
  173. path_attack_pcap = self.write_attack_pcap(packets, True, path_attack_pcap)
  174. last_packet = packets[-1]
  175. # Store timestamp of last packet
  176. self.attack_end_utime = last_packet.time
  177. # Return packets sorted by packet by timestamp and total number of packets (sent)
  178. return total_pkts , path_attack_pcap
  179. def _create_messages(self):
  180. def add_ids_to_config(ids_to_add: list, existing_ips: list, new_ips: list, bot_configs: dict, idtype:str="local", router_mac:str=""):
  181. """
  182. Creates IP and MAC configurations for the given IDs and adds them to the existing configurations object.
  183. :param ids_to_add: all sorted IDs that have to be configured and added
  184. :param existing_ips: the existing IPs in the PCAP file that should be assigned to some, or all, IDs
  185. :param new_ips: the newly generated IPs that should be assigned to some, or all, IDs
  186. :param bot_configs: the existing configurations for the bots
  187. :param idtype: the locality type of the IDs
  188. :param router_mac: the MAC address of the router in the PCAP
  189. """
  190. ids = ids_to_add.copy()
  191. # macgen only needed, when IPs are new local IPs (therefore creating the object here suffices for the current callers
  192. # to not end up with the same MAC paired with different IPs)
  193. macgen = MacAddressGenerator()
  194. # assign existing IPs and the corresponding MAC addresses in the PCAP to the IDs
  195. for ip in existing_ips:
  196. random_id = choice(ids)
  197. mac = self.statistics.process_db_query("macAddress(IPAddress=%s)" % ip)
  198. bot_configs[random_id] = {"Type": idtype, "IP": ip, "MAC": mac}
  199. ids.remove(random_id)
  200. # assign new IPs and for local IPs new MACs or for external IPs the router MAC to the IDs
  201. for ip in new_ips:
  202. random_id = choice(ids)
  203. if idtype == "local":
  204. mac = macgen.random_mac()
  205. elif idtype == "external":
  206. mac = router_mac
  207. bot_configs[random_id] = {"Type": idtype, "IP": ip, "MAC": mac}
  208. ids.remove(random_id)
  209. def index_increment(number: int, max: int):
  210. """
  211. Number increment with rollover.
  212. """
  213. if number + 1 < max:
  214. return number + 1
  215. else:
  216. return 0
  217. def assign_realistic_ttls2(bot_configs):
  218. # Gamma distribution parameters derived from MAWI 13.8G dataset
  219. ids = sorted(bot_configs.keys())
  220. alpha, loc, beta = (2.3261710235, -0.188306914406, 44.4853123884)
  221. gd = gamma.rvs(alpha, loc=loc, scale=beta, size=len(ids))
  222. for pos, bot in enumerate(ids):
  223. # print(bot)
  224. is_invalid = True
  225. pos_max = len(gd)
  226. while is_invalid:
  227. ttl = int(round(gd[pos]))
  228. if 0 < ttl < 256: # validity check
  229. is_invalid = False
  230. else:
  231. pos = index_increment(pos, pos_max)
  232. bot_configs[bot]["TTL"] = ttl
  233. def assign_realistic_ttls(bot_configs):
  234. '''
  235. Assigns a realisitic ttl to each bot from @param: bot_configs. Uses statistics and distribution to be able
  236. to calculate a realisitc ttl.
  237. :param bot_configs:
  238. :return:
  239. '''
  240. ids = sorted(bot_configs.keys())
  241. for pos,bot in enumerate(ids):
  242. #print(type(bot_configs))
  243. # Set TTL based on TTL distribution of IP address
  244. bot_ttl_dist = self.statistics.get_ttl_distribution(bot_configs[bot]["IP"])
  245. if len(bot_ttl_dist) > 0:
  246. source_ttl_prob_dict = Lea.fromValFreqsDict(bot_ttl_dist)
  247. bot_configs[bot]["TTL"] = source_ttl_prob_dict.random()
  248. else:
  249. bot_configs[bot]["TTL"] = self.statistics.process_db_query("most_used(ttlValue)")
  250. # parse input CSV or XML
  251. filepath_xml = self.get_param_value(Param.FILE_XML)
  252. filepath_csv = self.get_param_value(Param.FILE_CSV)
  253. # prefer XML input over CSV input (in case both are given)
  254. if filepath_csv and filepath_xml == self.DEFAULT_XML_PATH:
  255. filepath_xml = FileUtils.parse_csv_to_xml(filepath_csv)
  256. abstract_packets = FileUtils.parse_xml(filepath_xml)
  257. # find a good communication mapping in the input file that matches the users parameters
  258. duration = self.get_param_value(Param.ATTACK_DURATION)
  259. number_init_bots = self.get_param_value(Param.NUMBER_INITIATOR_BOTS)
  260. comm_proc = CommunicationProcessor(abstract_packets, self.msg_types)
  261. comm_intervals = comm_proc.find_interval_with_most_comm(number_init_bots, duration)
  262. if comm_intervals == []:
  263. print("Error: There is no interval in the given CSV/XML that has enough communication initiating bots.")
  264. return []
  265. comm_interval = comm_intervals[0]
  266. # retrieve the mapping information
  267. mapped_ids, packet_start_idx, packet_end_idx = comm_interval["IDs"], comm_interval["Start"], comm_interval["End"]
  268. # assign the communication processor this mapping for further processing
  269. comm_proc.set_mapping(abstract_packets[packet_start_idx:packet_end_idx+1], mapped_ids)
  270. # print start and end time of mapped interval
  271. # print(abstract_packets[packet_start_idx]["Time"])
  272. # print(abstract_packets[packet_end_idx]["Time"])
  273. # determine number of reused local and external IPs
  274. reuse_percent_total = self.get_param_value(Param.IP_REUSE_TOTAL)
  275. reuse_percent_external = self.get_param_value(Param.IP_REUSE_EXTERNAL)
  276. reuse_percent_local = self.get_param_value(Param.IP_REUSE_LOCAL)
  277. reuse_count_external = int(reuse_percent_total * reuse_percent_external * len(mapped_ids))
  278. reuse_count_local = int(reuse_percent_total * reuse_percent_local * len(mapped_ids))
  279. # create locality, IP and MAC configurations for the IDs/Bots
  280. ipgen = IPGenerator()
  281. pcapops = PcapAddressOperations(self.statistics)
  282. router_mac = pcapops.get_probable_router_mac()
  283. bot_configs = {}
  284. # determine the roles of the IDs in the mapping communication-{initiator, responder}
  285. init_ids, respnd_ids, messages = comm_proc.det_id_roles_and_msgs()
  286. # use these roles to determine which IDs are to be local and which external
  287. local_ids, external_ids = comm_proc.det_ext_and_local_ids(self.PROB_RESPND_IS_LOCAL)
  288. # retrieve and assign the IPs and MACs for the bots with respect to the given parameters
  289. # (IDs are always added to bot_configs in the same order under a given seed)
  290. number_local_ids, number_external_ids = len(local_ids), len(external_ids)
  291. # assign addresses for local IDs
  292. if number_local_ids > 0:
  293. reuse_count_local = int(reuse_percent_total * reuse_percent_local * number_local_ids)
  294. existing_local_ips = sorted(pcapops.get_existing_local_ips(reuse_count_local))
  295. new_local_ips = sorted(pcapops.get_new_local_ips(number_local_ids - len(existing_local_ips)))
  296. add_ids_to_config(sorted(local_ids), existing_local_ips, new_local_ips, bot_configs)
  297. # assign addresses for external IDs
  298. if number_external_ids > 0:
  299. reuse_count_external = int(reuse_percent_total * reuse_percent_external * number_external_ids)
  300. existing_external_ips = sorted(pcapops.get_existing_external_ips(reuse_count_external))
  301. remaining = len(external_ids) - len(existing_external_ips)
  302. new_external_ips = sorted([ipgen.random_ip() for _ in range(remaining)])
  303. add_ids_to_config(sorted(external_ids), existing_external_ips, new_external_ips, bot_configs, idtype="external", router_mac=router_mac)
  304. #### Set realistic timestamps for messages ####
  305. #### ... ####
  306. # create port configurations for the bots
  307. for bot in bot_configs:
  308. bot_configs[bot]["Port"] = gen_random_server_port()
  309. # print(init_ids)
  310. # print(bot_configs)
  311. # assign realistic TTL for every bot
  312. assign_realistic_ttls(bot_configs)
  313. # put together the final messages including the full sender and receiver
  314. # configurations (i.e. IP, MAC, port, ...) for easier later use
  315. final_messages = []
  316. new_id = 0
  317. for msg in messages:
  318. type_src, type_dst = bot_configs[msg.src]["Type"], bot_configs[msg.dst]["Type"]
  319. id_src, id_dst = msg.src, msg.dst
  320. # sort out messages that do not have a suitable locality setting
  321. if type_src == "external" and type_dst == "external":
  322. continue
  323. msg.src, msg.dst = bot_configs[id_src], bot_configs[id_dst]
  324. msg.src["ID"], msg.dst["ID"] = id_src, id_dst
  325. msg.msg_id = new_id
  326. new_id += 1
  327. ### Important here to update refers, if needed later?
  328. final_messages.append(msg)
  329. return final_messages
  330. def _get_capture_duration(self):
  331. """
  332. Returns the duration of the input PCAP (since statistics duration seems to be incorrect)
  333. """
  334. ts_date_format = "%Y-%m-%d %H:%M:%S.%f"
  335. ts_first_date = datetime.strptime(self.statistics.get_pcap_timestamp_start(), ts_date_format)
  336. ts_last_date = datetime.strptime(self.statistics.get_pcap_timestamp_end(), ts_date_format)
  337. diff_date = ts_last_date - ts_first_date
  338. duration = "%d.%d" % (diff_date.total_seconds(), diff_date.microseconds)
  339. return duration