BaseAttack.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import ipaddress
  2. import os
  3. import random
  4. import re
  5. import tempfile
  6. from abc import abstractmethod, ABCMeta
  7. import numpy as np
  8. import ID2TLib.libpcapreader as pr
  9. from scapy.utils import PcapWriter
  10. from Attack import AttackParameters
  11. from Attack.AttackParameters import Parameter
  12. from Attack.AttackParameters import ParameterTypes
  13. class BaseAttack(metaclass=ABCMeta):
  14. """
  15. Abstract base class for all attack classes. Provides basic functionalities, like parameter validation.
  16. """
  17. def __init__(self, statistics, name, description, attack_type):
  18. """
  19. To be called within the individual attack class to initialize the required parameters.
  20. :param statistics: A reference to the Statistics class.
  21. :param name: The name of the attack class.
  22. :param description: A short description of the attack.
  23. :param attack_type: The type the attack belongs to, like probing/scanning, malware.
  24. """
  25. # Reference to statistics class
  26. self.statistics = statistics
  27. # Class fields
  28. self.attack_name = name
  29. self.attack_description = description
  30. self.attack_type = attack_type
  31. self.params = {}
  32. self.supported_params = {}
  33. self.attack_start_utime = 0
  34. self.attack_end_utime = 0
  35. @abstractmethod
  36. def generate_attack_pcap(self):
  37. """
  38. Creates a pcap containing the attack packets.
  39. :return: The location of the generated pcap file.
  40. """
  41. pass
  42. ################################################
  43. # HELPER VALIDATION METHODS
  44. # Used to validate the given parameter values
  45. ################################################
  46. @staticmethod
  47. def _is_mac_address(mac_address: str):
  48. """
  49. Verifies if the given string is a valid MAC address. Accepts the formats 00:80:41:ae:fd:7e and 00-80-41-ae-fd-7e.
  50. :param mac_address: The MAC address as string.
  51. :return: True if the MAC address is valid, otherwise False.
  52. """
  53. pattern = re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', re.MULTILINE)
  54. if isinstance(mac_address, list):
  55. for mac in mac_address:
  56. if re.match(pattern, mac) is None:
  57. return False
  58. else:
  59. if re.match(pattern, mac_address) is None:
  60. return False
  61. return True
  62. @staticmethod
  63. def _is_ip_address(ip_address: str):
  64. """
  65. Verifies that the given string or list of IP addresses (strings) is a valid IPv4/IPv6 address.
  66. Accepts comma-separated lists of IP addresses, like "192.169.178.1, 192.168.178.2"
  67. :param ip_address: The IP address(es) as list of strings or comma-separated string.
  68. :return: True if all IP addresses are valid, otherwise False. And a list of IP addresses as string.
  69. """
  70. ip_address_output = []
  71. # a comma-separated list of IP addresses must be splitted first
  72. if isinstance(ip_address, str):
  73. ip_address = ip_address.split(',')
  74. for ip in ip_address:
  75. try:
  76. ipaddress.ip_address(ip)
  77. ip_address_output.append(ip)
  78. except ValueError:
  79. return False, ip_address_output
  80. if len(ip_address_output) == 1:
  81. return True, ip_address_output[0]
  82. else:
  83. return True, ip_address_output
  84. @staticmethod
  85. def _is_port(ports_input: str):
  86. """
  87. Verifies if the given value is a valid port. Accepts port ranges, like 80-90, 80..99, 80...99.
  88. :param ports_input: The port number as int or string.
  89. :return: True if the port number is valid, otherwise False. If a single port or a comma-separated list of ports
  90. was given, a list of int is returned. If a port range was given, the range is resolved
  91. and a list of int is returned.
  92. """
  93. def _is_invalid_port(num):
  94. """
  95. Checks whether the port number is invalid.
  96. :param num: The port number as int.
  97. :return: True if the port number is invalid, otherwise False.
  98. """
  99. return num < 1 or num > 65535
  100. if isinstance(ports_input, str):
  101. ports_input = ports_input.replace(' ', '').split(',')
  102. elif isinstance(ports_input, int):
  103. ports_input = [ports_input]
  104. ports_output = []
  105. for port_entry in ports_input:
  106. if isinstance(port_entry, int):
  107. if _is_invalid_port(port_entry):
  108. return False
  109. ports_output.append(port_entry)
  110. elif isinstance(port_entry, str) and port_entry.isdigit():
  111. # port_entry describes a single port
  112. port_entry = int(port_entry)
  113. if _is_invalid_port(port_entry):
  114. return False
  115. ports_output.append(port_entry)
  116. elif '-' in port_entry or '..' in port_entry:
  117. # port_entry describes a port range
  118. # allowed format: '1-49151', '1..49151', '1...49151'
  119. match = re.match('^([0-9]{1,5})(?:-|\.{2,3})([0-9]{1,5})$', port_entry)
  120. # check validity of port range
  121. # and create list of ports derived from given start and end port
  122. (port_start, port_end) = int(match.group(1)), int(match.group(2))
  123. if _is_invalid_port(port_start) or _is_invalid_port(port_end):
  124. return False
  125. else:
  126. ports_list = [i for i in range(port_start, port_end + 1)]
  127. # append ports at ports_output list
  128. ports_output += ports_list
  129. if len(ports_output) == 1:
  130. return True, ports_output[0]
  131. else:
  132. return True, ports_output
  133. @staticmethod
  134. def _is_timestamp(timestamp: str):
  135. """
  136. Checks whether the given value is in a valid timestamp format. The accepted format is:
  137. YYYY-MM-DD h:m:s, whereas h, m, s may be one or two digits.
  138. :param timestamp: The timestamp to be checked.
  139. :return: True if the timestamp is valid, otherwise False.
  140. """
  141. is_valid = re.match('[0-9]{4}(?:-[0-9]{1,2}){2} (?:[0-9]{1,2}:){2}[0-9]{1,2}', timestamp)
  142. return is_valid is not None
  143. @staticmethod
  144. def _is_boolean(value):
  145. """
  146. Checks whether the given value (string or bool) is a boolean. Strings are valid booleans if they are in:
  147. {y, yes, t, true, on, 1, n, no, f, false, off, 0}.
  148. :param value: The value to be checked.
  149. :return: True if the value is a boolean, otherwise false. And the casted boolean.
  150. """
  151. # If value is already a boolean
  152. if isinstance(value, bool):
  153. return True, value
  154. # If value is a string
  155. # True values are y, yes, t, true, on and 1;
  156. # False values are n, no, f, false, off and 0.
  157. # Raises ValueError if value is anything else.
  158. try:
  159. import distutils.core
  160. value = distutils.util.strtobool(value.lower())
  161. is_bool = True
  162. except ValueError:
  163. is_bool = False
  164. return is_bool, value
  165. @staticmethod
  166. def _is_float(value):
  167. """
  168. Checks whether the given value is a float.
  169. :param value: The value to be checked.
  170. :return: True if the value is a float, otherwise False. And the casted float.
  171. """
  172. try:
  173. value = float(value)
  174. return True, value
  175. except ValueError:
  176. return False, value
  177. #########################################
  178. # HELPER METHODS
  179. #########################################
  180. def add_param_value(self, param, value):
  181. """
  182. Adds the pair param : value to the dictionary of attack parameters. Prints and error message and skips the
  183. parameter if the validation fails.
  184. :param param: The parameter name.
  185. :param value: The parameter's value.
  186. :return: None.
  187. """
  188. # by default no param is valid
  189. is_valid = False
  190. # get AttackParameters instance associated with param
  191. # for default values assigned in attack classes, like Parameter.PORT_OPEN
  192. if isinstance(param, AttackParameters.Parameter):
  193. param_name = param
  194. # for values given by user input, like port.open
  195. else:
  196. # Get Enum key of given string identifier
  197. param_name = AttackParameters.Parameter(param)
  198. # Get parameter type of attack's required_params
  199. param_type = self.supported_params.get(param_name)
  200. # Verify validity of given value with respect to parameter type
  201. if param_type is None:
  202. print('Parameter ' + str(param_name) + ' not available for chosen attack. Skipping parameter.')
  203. # If value is query -> get value from database
  204. elif self.statistics.is_query(value):
  205. value = self.statistics.process_db_query(value, False)
  206. if value is not None and value is not "":
  207. is_valid = True
  208. else:
  209. print('Error in given parameter value: ' + value + '. Data could not be retrieved.')
  210. # Validate parameter depending on parameter's type
  211. elif param_type == ParameterTypes.TYPE_IP_ADDRESS:
  212. is_valid, value = self._is_ip_address(value)
  213. elif param_type == ParameterTypes.TYPE_PORT:
  214. is_valid, value = self._is_port(value)
  215. elif param_type == ParameterTypes.TYPE_MAC_ADDRESS:
  216. is_valid = self._is_mac_address(value)
  217. elif param_type == ParameterTypes.TYPE_INTEGER_POSITIVE:
  218. if isinstance(value, int) and int(value) >= 0:
  219. is_valid = True
  220. elif isinstance(value, str) and value.isdigit() and int(value) >= 0:
  221. is_valid = True
  222. value = int(value)
  223. elif param_type == ParameterTypes.TYPE_FLOAT:
  224. is_valid, value = self._is_float(value)
  225. # this is required to avoid that the timestamp's microseconds of the first attack packet is '000000'
  226. # but microseconds are only chosen randomly if the given parameter does not already specify it
  227. # e.g. inject.at-timestamp=123456.987654 -> is not changed
  228. # e.g. inject.at-timestamp=123456 -> is changed to: 123456.[random digits]
  229. if param_name == Parameter.INJECT_AT_TIMESTAMP and is_valid and ((value - int(value)) == 0):
  230. value = value + random.uniform(0, 0.999999)
  231. elif param_type == ParameterTypes.TYPE_TIMESTAMP:
  232. is_valid = self._is_timestamp(value)
  233. elif param_type == ParameterTypes.TYPE_BOOLEAN:
  234. is_valid, value = self._is_boolean(value)
  235. elif param_type == ParameterTypes.TYPE_PACKET_POSITION:
  236. ts = pr.pcap_processor(self.statistics.pcap_filepath).get_timestamp_mu_sec(int(value))
  237. if 0 <= int(value) <= self.statistics.get_packet_count() and ts >= 0:
  238. is_valid = True
  239. param_name = Parameter.INJECT_AT_TIMESTAMP
  240. value = (ts / 1000000) # convert microseconds from getTimestampMuSec into seconds
  241. # add value iff validation was successful
  242. if is_valid:
  243. self.params[param_name] = value
  244. else:
  245. print("ERROR: Parameter " + str(param) + " or parameter value " + str(value) +
  246. " not valid. Skipping parameter.")
  247. def get_param_value(self, param: Parameter):
  248. """
  249. Returns the parameter value for a given parameter.
  250. :param param: The parameter whose value is wanted.
  251. :return: The parameter's value.
  252. """
  253. return self.params.get(param)
  254. def check_parameters(self):
  255. """
  256. Checks whether all parameter values are defined. If a value is not defined, the application is terminated.
  257. However, this should not happen as all attack should define default parameter values.
  258. """
  259. # parameters which do not require default values
  260. non_obligatory_params = [Parameter.INJECT_AFTER_PACKET, Parameter.NUMBER_ATTACKERS]
  261. for param, type in self.supported_params.items():
  262. # checks whether all params have assigned values, INJECT_AFTER_PACKET must not be considered because the
  263. # timestamp derived from it is set to Parameter.INJECT_AT_TIMESTAMP
  264. if param not in self.params.keys() and param not in non_obligatory_params:
  265. print("\033[91mCRITICAL ERROR: Attack '" + self.attack_name + "' does not define the parameter '" +
  266. str(param) + "'.\n The attack must define default values for all parameters."
  267. + "\n Cannot continue attack generation.\033[0m")
  268. import sys
  269. sys.exit(0)
  270. def write_attack_pcap(self, packets: list, append_flag: bool = False, destination_path: str = None):
  271. """
  272. Writes the attack's packets into a PCAP file with a temporary filename.
  273. :return: The path of the written PCAP file.
  274. """
  275. # Only check params initially when attack generation starts
  276. if append_flag is False and destination_path is None:
  277. # Check if all req. parameters are set
  278. self.check_parameters()
  279. # Determine destination path
  280. if destination_path is not None and os.path.exists(destination_path):
  281. destination = destination_path
  282. else:
  283. temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pcap')
  284. destination = temp_file.name
  285. # Write packets into pcap file
  286. pktdump = PcapWriter(destination, append=append_flag)
  287. pktdump.write(packets)
  288. # Store pcap path and close file objects
  289. pktdump.close()
  290. return destination
  291. #########################################
  292. # RANDOM IP/MAC ADDRESS GENERATORS
  293. #########################################
  294. @staticmethod
  295. def generate_random_ipv4_address(ipClass, n: int = 1):
  296. """
  297. Generates n random IPv4 addresses.
  298. :param n: The number of IP addresses to be generated
  299. :return: A single IP address, or if n>1, a list of IP addresses
  300. """
  301. def is_invalid(ipAddress: ipaddress.IPv4Address):
  302. return ipAddress.is_multicast or ipAddress.is_unspecified or ipAddress.is_loopback or \
  303. ipAddress.is_link_local or ipAddress.is_reserved or ipAddress.is_private
  304. # Aidmar - generate a random IP from specific class
  305. #def generate_address():
  306. # return ipaddress.IPv4Address(random.randint(0, 2 ** 32 - 1))
  307. def generate_address(ipClass):
  308. #print(ipClass)
  309. """if "private" in ipClass:
  310. ipClassesByte1 = {"A-private": 10, "B-private": 172, "C-private": 192}
  311. b1 = ipClassesByte1[ipClass]
  312. ipClassesByte2 = {"A-private": {0,255}, "B-private": {16,131}, "C-private": {168,168}}
  313. minB2 = ipClassesByte1[ipClass][0]
  314. maxB2 = ipClassesByte1[ipClass][1]
  315. b2 = random.randint(minB2, maxB2)
  316. b3b4 = random.randint(0, 2 ** 16 - 1)
  317. ipAddress = ipaddress.IPv4Address(str(b1)+str(b2)+str(b3b4))
  318. else:"""
  319. if ipClass == "Unknown":
  320. return ipaddress.IPv4Address(random.randint(0, 2 ** 32 - 1))
  321. else:
  322. # For DDoS attack, we do not generate private IPs
  323. if "private" in ipClass:
  324. ipClass = ipClass[0] # convert A-private to A
  325. ipClassesByte1 = {"A": {1,126}, "B": {128,191}, "C":{192, 223}, "D":{224, 239}, "E":{240, 254}}
  326. temp = list(ipClassesByte1[ipClass])
  327. minB1 = temp[0]
  328. maxB1 = temp[1]
  329. b1 = random.randint(minB1, maxB1)
  330. b2 = random.randint(1, 255)
  331. b3 = random.randint(1, 255)
  332. b4 = random.randint(1, 255)
  333. ipAddress = ipaddress.IPv4Address(str(b1) +"."+ str(b2) + "." + str(b3) + "." + str(b4))
  334. return ipAddress
  335. ip_addresses = []
  336. for i in range(0, n):
  337. address = generate_address(ipClass)
  338. while is_invalid(address):
  339. address = generate_address(ipClass)
  340. ip_addresses.append(str(address))
  341. if n == 1:
  342. return ip_addresses[0]
  343. else:
  344. return ip_addresses
  345. @staticmethod
  346. def generate_random_ipv6_address(n: int = 1):
  347. """
  348. Generates n random IPv6 addresses.
  349. :param n: The number of IP addresses to be generated
  350. :return: A single IP address, or if n>1, a list of IP addresses
  351. """
  352. def is_invalid(ipAddress: ipaddress.IPv6Address):
  353. return ipAddress.is_multicast or ipAddress.is_unspecified or ipAddress.is_loopback or \
  354. ipAddress.is_link_local or ipAddress.is_private or ipAddress.is_reserved
  355. def generate_address():
  356. return ipaddress.IPv6Address(random.randint(0, 2 ** 128 - 1))
  357. ip_addresses = []
  358. for i in range(0, n):
  359. address = generate_address()
  360. while is_invalid(address):
  361. address = generate_address()
  362. ip_addresses.append(str(address))
  363. if n == 1:
  364. return ip_addresses[0]
  365. else:
  366. return ip_addresses
  367. @staticmethod
  368. def generate_random_mac_address(n: int = 1):
  369. """
  370. Generates n random MAC addresses.
  371. :param n: The number of MAC addresses to be generated.
  372. :return: A single MAC addres, or if n>1, a list of MAC addresses
  373. """
  374. def is_invalid(address: str):
  375. first_octet = int(address[0:2], 16)
  376. is_multicast_address = bool(first_octet & 0b01)
  377. is_locally_administered = bool(first_octet & 0b10)
  378. return is_multicast_address or is_locally_administered
  379. def generate_address():
  380. mac = [random.randint(0x00, 0xff) for i in range(0, 6)]
  381. return ':'.join(map(lambda x: "%02x" % x, mac))
  382. mac_addresses = []
  383. for i in range(0, n):
  384. address = generate_address()
  385. while is_invalid(address):
  386. address = generate_address()
  387. mac_addresses.append(address)
  388. if n == 1:
  389. return mac_addresses[0]
  390. else:
  391. return mac_addresses
  392. # Aidmar
  393. def get_reply_delay(self, ip_dst):
  394. replyDelay = self.statistics.process_db_query(
  395. "SELECT avgDelay FROM conv_statistics WHERE ipAddressB='" + ip_dst + "' LIMIT 1")
  396. if not replyDelay:
  397. allDelays = self.statistics.process_db_query("SELECT avgDelay FROM conv_statistics")
  398. replyDelay = np.median(allDelays)
  399. replyDelay = int(replyDelay) * 10 ** -6 # convert from micro to seconds
  400. #print(replyDelay)
  401. return replyDelay