MembersMgmtCommAttack.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. from enum import Enum
  2. from random import randint, randrange, choice, uniform
  3. from collections import deque
  4. from scipy.stats import gamma
  5. from lea import Lea
  6. from datetime import datetime
  7. import os
  8. from Attack import BaseAttack
  9. from Attack.AttackParameters import Parameter as Param
  10. from Attack.AttackParameters import ParameterTypes
  11. class MessageType(Enum):
  12. """
  13. Defines possible botnet message types
  14. """
  15. TIMEOUT = 3
  16. SALITY_NL_REQUEST = 101
  17. SALITY_NL_REPLY = 102
  18. SALITY_HELLO = 103
  19. SALITY_HELLO_REPLY = 104
  20. def is_request(mtype):
  21. return mtype in {MessageType.SALITY_HELLO, MessageType.SALITY_NL_REQUEST}
  22. def is_response(mtype):
  23. return mtype in {MessageType.SALITY_HELLO_REPLY, MessageType.SALITY_NL_REPLY}
  24. class Message():
  25. INVALID_LINENO = -1
  26. """
  27. Defines a compact message type that contains all necessary information.
  28. """
  29. def __init__(self, msg_id: int, src, dst, type_: MessageType, time: float, refer_msg_id: int=-1, line_no = -1):
  30. """
  31. Constructs a message with the given parameters.
  32. :param msg_id: the ID of the message
  33. :param src: something identifiying the source, e.g. ID or configuration
  34. :param dst: something identifiying the destination, e.g. ID or configuration
  35. :param type_: the type of the message
  36. :param time: the timestamp of the message
  37. :param refer_msg_id: the ID this message is a request for or reply to. -1 if there is no related message.
  38. :param line_no: The line number this message appeared in the original file
  39. """
  40. self.msg_id = msg_id
  41. self.src = src
  42. self.dst = dst
  43. self.type = type_
  44. self.time = time
  45. self.refer_msg_id = refer_msg_id
  46. # if similar fields to line_no should be added consider a separate class
  47. self.line_no = line_no
  48. def __str__(self):
  49. 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)
  50. return str_
  51. from ID2TLib import FileUtils, Generator
  52. from ID2TLib.PcapAddressOperations import PcapAddressOperations
  53. from ID2TLib.CommunicationProcessor import CommunicationProcessor
  54. from ID2TLib.Botnet.MessageMapping import MessageMapping
  55. class MembersMgmtCommAttack(BaseAttack.BaseAttack):
  56. def __init__(self):
  57. """
  58. Creates a new instance of the Membership Management Communication.
  59. """
  60. # Initialize communication
  61. super(MembersMgmtCommAttack, self).__init__("Membership Management Communication Attack (MembersMgmtCommAttack)",
  62. "Injects Membership Management Communication", "Botnet communication")
  63. # Define allowed parameters and their type
  64. self.supported_params = {
  65. # parameters regarding attack
  66. Param.INJECT_AT_TIMESTAMP: ParameterTypes.TYPE_FLOAT,
  67. Param.INJECT_AFTER_PACKET: ParameterTypes.TYPE_PACKET_POSITION,
  68. Param.PACKETS_PER_SECOND: ParameterTypes.TYPE_FLOAT,
  69. Param.PACKETS_LIMIT: ParameterTypes.TYPE_INTEGER_POSITIVE,
  70. Param.ATTACK_DURATION: ParameterTypes.TYPE_INTEGER_POSITIVE,
  71. # use num_attackers to specify number of communicating devices?
  72. Param.NUMBER_INITIATOR_BOTS: ParameterTypes.TYPE_INTEGER_POSITIVE,
  73. # input file containing botnet communication
  74. Param.FILE_CSV: ParameterTypes.TYPE_FILEPATH,
  75. Param.FILE_XML: ParameterTypes.TYPE_FILEPATH,
  76. # the percentage of IP reuse (if total and other is specified, percentages are multiplied)
  77. Param.IP_REUSE_TOTAL: ParameterTypes.TYPE_PERCENTAGE,
  78. Param.IP_REUSE_LOCAL: ParameterTypes.TYPE_PERCENTAGE,
  79. Param.IP_REUSE_EXTERNAL: ParameterTypes.TYPE_PERCENTAGE,
  80. # the user-selected padding to add to every packet
  81. Param.PACKET_PADDING: ParameterTypes.TYPE_PADDING,
  82. # presence of NAT at the gateway of the network
  83. Param.NAT_PRESENT: ParameterTypes.TYPE_BOOLEAN
  84. }
  85. # create dict with MessageType values for fast name lookup
  86. self.msg_types = {}
  87. for msg_type in MessageType:
  88. self.msg_types[msg_type.value] = msg_type
  89. def init_params(self):
  90. """
  91. Initialize some parameters of this communication-attack using the user supplied command line parameters.
  92. The remaining parameters are implicitly set in the provided data file. Note: the timestamps in the file
  93. have to be sorted in ascending order
  94. :param statistics: Reference to a statistics object.
  95. """
  96. # set class constants
  97. self.DEFAULT_XML_PATH = "resources/MembersMgmtComm_example.xml"
  98. # probability for responder ID to be local if comm_type is mixed
  99. self.PROB_RESPND_IS_LOCAL = 0
  100. # PARAMETERS: initialize with default values
  101. # (values are overwritten if user specifies them)
  102. self.add_param_value(Param.INJECT_AFTER_PACKET, randint(1, int(self.statistics.get_packet_count()/5)))
  103. self.add_param_value(Param.PACKETS_PER_SECOND, 0)
  104. self.add_param_value(Param.FILE_XML, self.DEFAULT_XML_PATH)
  105. # Alternatively new attack parameter?
  106. duration = int(float(self._get_capture_duration()))
  107. self.add_param_value(Param.ATTACK_DURATION, duration)
  108. self.add_param_value(Param.NUMBER_INITIATOR_BOTS, 1)
  109. # NAT on by default
  110. self.add_param_value(Param.NAT_PRESENT, True)
  111. # default locality behavior
  112. # self.add_param_value(Param.COMM_TYPE, "mixed")
  113. # TODO: change 1 to something better
  114. self.add_param_value(Param.IP_REUSE_TOTAL, 1)
  115. self.add_param_value(Param.IP_REUSE_LOCAL, 0.5)
  116. self.add_param_value(Param.IP_REUSE_EXTERNAL, 0.5)
  117. # add default additional padding
  118. self.add_param_value(Param.PACKET_PADDING, 20)
  119. def generate_attack_pcap(self, context):
  120. # create the final messages that have to be sent, including all bot configurations
  121. messages = self._create_messages(context)
  122. if messages == []:
  123. return 0, []
  124. # Setup (initial) parameters for packet creation loop
  125. BUFFER_SIZE = 1000
  126. pkt_gen = Generator.PacketGenerator()
  127. padding = self.get_param_value(Param.PACKET_PADDING)
  128. packets = deque(maxlen=BUFFER_SIZE)
  129. total_pkts = 0
  130. limit_packetcount = self.get_param_value(Param.PACKETS_LIMIT)
  131. limit_duration = self.get_param_value(Param.ATTACK_DURATION)
  132. path_attack_pcap = None
  133. msg_packet_mapping = MessageMapping(messages)
  134. # create packets to write to PCAP file
  135. for msg in messages:
  136. # retrieve the source and destination configurations
  137. id_src, id_dst = msg.src["ID"], msg.dst["ID"]
  138. ip_src, ip_dst = msg.src["IP"], msg.dst["IP"]
  139. mac_src, mac_dst = msg.src["MAC"], msg.dst["MAC"]
  140. port_src, port_dst = msg.src["Port"], msg.dst["Port"]
  141. ttl = msg.src["TTL"]
  142. # update duration
  143. duration = msg.time - messages[0].time
  144. # if total number of packets has been sent or the attack duration has been exceeded, stop
  145. if ((limit_packetcount is not None and total_pkts >= limit_packetcount) or
  146. (limit_duration is not None and duration >= limit_duration)):
  147. break
  148. # if the type of the message is a NL reply, determine the number of entries
  149. nl_size = 0
  150. if msg.type == MessageType.SALITY_NL_REPLY:
  151. nl_size = randint(1, 25) # what is max NL entries?
  152. # create suitable IP/UDP packet and add to packets list
  153. packet = pkt_gen.generate_mmcom_packet(ip_src=ip_src, ip_dst=ip_dst, ttl=ttl, mac_src=mac_src, mac_dst=mac_dst,
  154. port_src=port_src, port_dst=port_dst, message_type=msg.type, neighborlist_entries=nl_size)
  155. Generator.add_padding(packet, padding,True, True)
  156. packet.time = msg.time
  157. packets.append(packet)
  158. msg_packet_mapping.map_message(msg, 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. Generator.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. Generator.equal_length(packets, padding = padding)
  173. path_attack_pcap = self.write_attack_pcap(packets, True, path_attack_pcap)
  174. last_packet = packets[-1]
  175. # write the mapping to a file
  176. msg_packet_mapping.write_to(context.allocate_file("_mapping.xml"))
  177. # Store timestamp of last packet
  178. self.attack_end_utime = last_packet.time
  179. # Return packets sorted by packet by timestamp and total number of packets (sent)
  180. return total_pkts , path_attack_pcap
  181. def _create_messages(self, context):
  182. def add_ids_to_config(ids_to_add: list, existing_ips: list, new_ips: list, bot_configs: dict, idtype:str="local", router_mac:str=""):
  183. """
  184. Creates IP and MAC configurations for the given IDs and adds them to the existing configurations object.
  185. :param ids_to_add: all sorted IDs that have to be configured and added
  186. :param existing_ips: the existing IPs in the PCAP file that should be assigned to some, or all, IDs
  187. :param new_ips: the newly generated IPs that should be assigned to some, or all, IDs
  188. :param bot_configs: the existing configurations for the bots
  189. :param idtype: the locality type of the IDs
  190. :param router_mac: the MAC address of the router in the PCAP
  191. """
  192. ids = ids_to_add.copy()
  193. # macgen only needed, when IPs are new local IPs (therefore creating the object here suffices for the current callers
  194. # to not end up with the same MAC paired with different IPs)
  195. macgen = Generator.MacAddressGenerator()
  196. # assign existing IPs and the corresponding MAC addresses in the PCAP to the IDs
  197. for ip in existing_ips:
  198. random_id = choice(ids)
  199. mac = self.statistics.process_db_query("macAddress(IPAddress=%s)" % ip)
  200. bot_configs[random_id] = {"Type": idtype, "IP": ip, "MAC": mac}
  201. ids.remove(random_id)
  202. # assign new IPs and for local IPs new MACs or for external IPs the router MAC to the IDs
  203. for ip in new_ips:
  204. random_id = choice(ids)
  205. if idtype == "local":
  206. mac = macgen.random_mac()
  207. elif idtype == "external":
  208. mac = router_mac
  209. bot_configs[random_id] = {"Type": idtype, "IP": ip, "MAC": mac}
  210. ids.remove(random_id)
  211. def index_increment(number: int, max: int):
  212. """
  213. Number increment with rollover.
  214. """
  215. if number + 1 < max:
  216. return number + 1
  217. else:
  218. return 0
  219. def assign_realistic_ttls(bot_configs):
  220. '''
  221. Assigns a realisitic ttl to each bot from @param: bot_configs. Uses statistics and distribution to be able
  222. to calculate a realisitc ttl.
  223. :param bot_configs:
  224. :return:
  225. '''
  226. ids = sorted(bot_configs.keys())
  227. for pos,bot in enumerate(ids):
  228. bot_type = bot_configs[bot]["Type"]
  229. # print(bot_type)
  230. if(bot_type == "local"): # Set fix TTL for local Bots
  231. bot_configs[bot]["TTL"] = 128
  232. # Set TTL based on TTL distribution of IP address
  233. else: # Set varying TTl for external Bots
  234. bot_ttl_dist = self.statistics.get_ttl_distribution(bot_configs[bot]["IP"])
  235. if len(bot_ttl_dist) > 0:
  236. source_ttl_prob_dict = Lea.fromValFreqsDict(bot_ttl_dist)
  237. bot_configs[bot]["TTL"] = source_ttl_prob_dict.random()
  238. else:
  239. bot_configs[bot]["TTL"] = self.statistics.process_db_query("most_used(ttlValue)")
  240. def add_delay(timestamp: float, minDelay: float, delay: float):
  241. '''
  242. Adds delay to a timestamp, with a minimum value of minDelay. But usually a value close to delay
  243. :param timestamp: the timestamp that is to be increased
  244. :param minDelay: the minimum value that is to be added to the timestamp
  245. :param delay: The general size of the delay. Statistically speaking: the expected value
  246. :return: the updated timestamp
  247. '''
  248. randomdelay = Lea.fromValFreqsDict({0.15*delay: 7, 0.3*delay: 10, 0.7*delay:20,
  249. delay:33, 1.2*delay:20, 1.6*delay: 10, 1.9*delay: 7, 2.5*delay: 3, 4*delay: 1})
  250. if 0.1*delay < minDelay:
  251. print("Warning: minDelay probably too big when computing time_stamps")
  252. # updated timestamps consist of the sum of the minimum delay, the magnitude of the delay
  253. # and a deviation by up to 10% in order to guarantee uniqueness
  254. general_offset = randomdelay.random()
  255. unique_offset = uniform(-0.1*general_offset, 0.1*general_offset)
  256. return timestamp + minDelay + general_offset + unique_offset
  257. def move_xml_to_outdir(filepath_xml: str):
  258. """
  259. Moves the XML file at filepath_xml to the output directory of the PCAP
  260. :param filepath_xml: the filepath to the XML file
  261. :return: the new filepath to the XML file
  262. """
  263. pcap_dir = context.get_output_dir()
  264. xml_name = os.path.basename(filepath_xml)
  265. if pcap_dir.endswith("/"):
  266. new_xml_path = pcap_dir + xml_name
  267. else:
  268. new_xml_path = pcap_dir + "/" + xml_name
  269. os.rename(filepath_xml, new_xml_path)
  270. context.add_other_created_file(new_xml_path)
  271. return new_xml_path
  272. # parse input CSV or XML
  273. filepath_xml = self.get_param_value(Param.FILE_XML)
  274. filepath_csv = self.get_param_value(Param.FILE_CSV)
  275. # prefer XML input over CSV input (in case both are given)
  276. if filepath_csv and filepath_xml == self.DEFAULT_XML_PATH:
  277. filepath_xml = FileUtils.parse_csv_to_xml(filepath_csv)
  278. filepath_xml = move_xml_to_outdir(filepath_xml)
  279. abstract_packets = FileUtils.parse_xml(filepath_xml)
  280. # find a good communication mapping in the input file that matches the users parameters
  281. duration = self.get_param_value(Param.ATTACK_DURATION)
  282. number_init_bots = self.get_param_value(Param.NUMBER_INITIATOR_BOTS)
  283. nat = self.get_param_value(Param.NAT_PRESENT)
  284. comm_proc = CommunicationProcessor(abstract_packets, self.msg_types, nat)
  285. comm_intervals = comm_proc.find_interval_most_comm(number_init_bots, duration)
  286. if comm_intervals == []:
  287. print("Error: There is no interval in the given CSV/XML that has enough communication initiating bots.")
  288. return []
  289. comm_interval = comm_intervals[randrange(0, len(comm_intervals))]
  290. # retrieve the mapping information
  291. mapped_ids, packet_start_idx, packet_end_idx = comm_interval["IDs"], comm_interval["Start"], comm_interval["End"]
  292. # print(mapped_ids)
  293. while len(mapped_ids) > number_init_bots:
  294. rm_idx = randrange(0, len(mapped_ids))
  295. del mapped_ids[rm_idx]
  296. # assign the communication processor this mapping for further processing
  297. comm_proc.set_mapping(abstract_packets[packet_start_idx:packet_end_idx+1], mapped_ids)
  298. # print start and end time of mapped interval
  299. # print(abstract_packets[packet_start_idx]["Time"])
  300. # print(abstract_packets[packet_end_idx]["Time"])
  301. # print(mapped_ids)
  302. # determine number of reused local and external IPs
  303. reuse_percent_total = self.get_param_value(Param.IP_REUSE_TOTAL)
  304. reuse_percent_external = self.get_param_value(Param.IP_REUSE_EXTERNAL)
  305. reuse_percent_local = self.get_param_value(Param.IP_REUSE_LOCAL)
  306. reuse_count_external = int(reuse_percent_total * reuse_percent_external * len(mapped_ids))
  307. reuse_count_local = int(reuse_percent_total * reuse_percent_local * len(mapped_ids))
  308. # create locality, IP and MAC configurations for the IDs/Bots
  309. ipgen = Generator.IPGenerator()
  310. pcapops = PcapAddressOperations(self.statistics)
  311. router_mac = pcapops.get_probable_router_mac()
  312. bot_configs = {}
  313. # determine the roles of the IDs in the mapping communication-{initiator, responder}
  314. local_init_ids, external_init_ids, respnd_ids, messages = comm_proc.det_id_roles_and_msgs()
  315. # use these roles to determine which IDs are to be local and which external
  316. local_ids, external_ids = comm_proc.det_ext_and_local_ids()
  317. # retrieve and assign the IPs and MACs for the bots with respect to the given parameters
  318. # (IDs are always added to bot_configs in the same order under a given seed)
  319. number_local_ids, number_external_ids = len(local_ids), len(external_ids)
  320. # assign addresses for local IDs
  321. if number_local_ids > 0:
  322. reuse_count_local = int(reuse_percent_total * reuse_percent_local * number_local_ids)
  323. existing_local_ips = sorted(pcapops.get_existing_local_ips(reuse_count_local))
  324. new_local_ips = sorted(pcapops.get_new_local_ips(number_local_ids - len(existing_local_ips)))
  325. add_ids_to_config(sorted(local_ids), existing_local_ips, new_local_ips, bot_configs)
  326. # assign addresses for external IDs
  327. if number_external_ids > 0:
  328. reuse_count_external = int(reuse_percent_total * reuse_percent_external * number_external_ids)
  329. existing_external_ips = sorted(pcapops.get_existing_external_ips(reuse_count_external))
  330. remaining = len(external_ids) - len(existing_external_ips)
  331. new_external_ips = sorted([ipgen.random_ip() for _ in range(remaining)])
  332. add_ids_to_config(sorted(external_ids), existing_external_ips, new_external_ips, bot_configs, idtype="external", router_mac=router_mac)
  333. #### Set realistic timestamps for messages ####
  334. most_used_ip_address = self.statistics.get_most_used_ip_address()
  335. minDelay = self.get_reply_delay(most_used_ip_address)[0]
  336. next_timestamp = self.get_param_value(Param.INJECT_AT_TIMESTAMP)
  337. pcap_duration = float(self._get_capture_duration())
  338. equi_timeslice = pcap_duration/len(messages)
  339. # Dict, takes a tuple of 2 Bot_IDs as a key (ID with lower number first), returns the time when the Hello_reply came in
  340. hello_times = {}
  341. # msg_IDs with already updated timestamps
  342. updated_msgs = []
  343. for req_msg in messages:
  344. updated = 0
  345. if(req_msg.msg_id in updated_msgs):
  346. # message already updated
  347. continue
  348. if(req_msg.msg_id == -1):
  349. # message has no corresponding request/response
  350. req_msg.time = next_timestamp
  351. next_timestamp = add_delay(next_timestamp, minDelay, equi_timeslice)
  352. updated_msgs.append(req_msg.msg_id)
  353. continue
  354. elif req_msg.type != MessageType.SALITY_HELLO:
  355. # Hello messages must have preceded, so make sure the timestamp of this msg is after the HELLO_REPLY
  356. if int(req_msg.src) < int(req_msg.dst):
  357. hello_time = hello_times[(req_msg.src, req_msg.dst)]
  358. else:
  359. hello_time = hello_times[(req_msg.dst, req_msg.src)]
  360. if next_timestamp < hello_time:
  361. # use the time of the hello_reply instead of next_timestamp to update this pair of messages
  362. post_hello = add_delay(hello_time, minDelay, equi_timeslice)
  363. respns_msg = messages[req_msg.refer_msg_id]
  364. respns_msg.time = add_delay(post_hello, minDelay, equi_timeslice)
  365. req_msg.time = post_hello
  366. updated = 1
  367. if not updated:
  368. # update normally
  369. respns_msg = messages[req_msg.refer_msg_id]
  370. respns_msg.time = add_delay(next_timestamp, minDelay, equi_timeslice)
  371. req_msg.time = next_timestamp
  372. next_timestamp = add_delay(next_timestamp, minDelay, equi_timeslice)
  373. updated_msgs.append(req_msg.msg_id)
  374. updated_msgs.append(req_msg.refer_msg_id)
  375. if req_msg.type == MessageType.SALITY_HELLO:
  376. # if hello messages have been exchanged, save timestamp of the HELLO_REPLY
  377. if int(req_msg.src) < int(req_msg.dst):
  378. hello_times[(req_msg.src, req_msg.dst)] = respns_msg.time
  379. else:
  380. hello_times[(req_msg.dst, req_msg.src)] = respns_msg.time
  381. # create port configurations for the bots
  382. for bot in bot_configs:
  383. bot_configs[bot]["Port"] = Generator.gen_random_server_port()
  384. # print(local_init_ids)
  385. # print(bot_configs)
  386. # assign realistic TTL for every bot
  387. assign_realistic_ttls(bot_configs)
  388. # put together the final messages including the full sender and receiver
  389. # configurations (i.e. IP, MAC, port, ...) for easier later use
  390. final_messages = []
  391. messages = sorted(messages, key=lambda msg: msg.time)
  392. new_id = 0
  393. for msg in messages:
  394. type_src, type_dst = bot_configs[msg.src]["Type"], bot_configs[msg.dst]["Type"]
  395. id_src, id_dst = msg.src, msg.dst
  396. # sort out messages that do not have a suitable locality setting
  397. if type_src == "external" and type_dst == "external":
  398. continue
  399. msg.src, msg.dst = bot_configs[id_src], bot_configs[id_dst]
  400. msg.src["ID"], msg.dst["ID"] = id_src, id_dst
  401. msg.msg_id = new_id
  402. new_id += 1
  403. ### Important here to update refers, if needed later?
  404. final_messages.append(msg)
  405. return final_messages
  406. def _get_capture_duration(self):
  407. """
  408. Returns the duration of the input PCAP (since statistics duration seems to be incorrect)
  409. """
  410. ts_date_format = "%Y-%m-%d %H:%M:%S.%f"
  411. ts_first_date = datetime.strptime(self.statistics.get_pcap_timestamp_start(), ts_date_format)
  412. ts_last_date = datetime.strptime(self.statistics.get_pcap_timestamp_end(), ts_date_format)
  413. diff_date = ts_last_date - ts_first_date
  414. duration = "%d.%d" % (diff_date.total_seconds(), diff_date.microseconds)
  415. return duration