MembersMgmtCommAttack.py 22 KB

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