Utility.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import calendar as cal
  2. import datetime as dt
  3. import ipaddress
  4. import os
  5. import random as rnd
  6. import lea
  7. import xdg.BaseDirectory as BaseDir
  8. import scapy.layers.inet as inet
  9. import scipy.stats as stats
  10. import pytz as pytz
  11. CACHE_DIR = os.path.join(BaseDir.xdg_cache_home, 'id2t')
  12. CODE_DIR = os.path.dirname(os.path.abspath(__file__)) + "/../"
  13. ROOT_DIR = CODE_DIR + "../"
  14. RESOURCE_DIR = ROOT_DIR + "resources/"
  15. TEST_DIR = RESOURCE_DIR + "test/"
  16. OUT_DIR = None
  17. BOTNET_PCAP = RESOURCE_DIR + "2017-11-23_win16_cut_bot_udp.pcap"
  18. # List of common operation systems
  19. platforms = {"win7", "win10", "winxp", "win8.1", "macos", "linux", "win8", "winvista", "winnt", "win2000"}
  20. # Distribution of common operation systems
  21. platform_probability = {"win7": 48.43, "win10": 27.99, "winxp": 6.07, "win8.1": 6.07, "macos": 5.94, "linux": 3.38,
  22. "win8": 1.35, "winvista": 0.46, "winnt": 0.31}
  23. # List of no-ops
  24. x86_nops = {b'\x90', b'\xfc', b'\xfd', b'\xf8', b'\xf9', b'\xf5', b'\x9b'}
  25. # List of pseudo no-ops (includes ops which won't change the state e.g. read access)
  26. x86_pseudo_nops = {b'\x97', b'\x96', b'\x95', b'\x93', b'\x92', b'\x91', b'\x99', b'\x4d', b'\x48', b'\x47', b'\x4f',
  27. b'\x40', b'\x41', b'\x37', b'\x3f', b'\x27', b'\x2f', b'\x46', b'\x4e', b'\x98', b'\x9f', b'\x4a',
  28. b'\x44', b'\x42', b'\x43', b'\x49', b'\x4b', b'\x45', b'\x4c', b'\x60', b'\x0e', b'\x1e', b'\x50',
  29. b'\x55', b'\x53', b'\x51', b'\x57', b'\x52', b'\x06', b'\x56', b'\x54', b'\x16', b'\x58', b'\x5d',
  30. b'\x5b', b'\x59', b'\x5f', b'\x5a', b'\x5e', b'\xd6'}
  31. # Characters which result in operational behaviour (e.g. FTPWinaXeExploit.py)
  32. forbidden_chars = [b'\x00', b'\x0a', b'\x0d']
  33. # Used in get_attacker_config
  34. attacker_port_mapping = {}
  35. # Used in get_attacker_config
  36. attacker_ttl_mapping = {}
  37. # Identifier for attacks
  38. generic_attack_names = {"attack", "exploit"}
  39. def update_timestamp(timestamp: float, pps: float, delay: float=0, inj_pps: float=0, inj_timestamp: float=0):
  40. """
  41. Calculates the next timestamp to be used based on the packet per second rate (pps) and the maximum delay.
  42. :return: Timestamp to be used for the next packet.
  43. """
  44. # FIXME: throw Exception if pps==0
  45. second = 0
  46. packets_this_second = 0
  47. if inj_pps != 0 and inj_timestamp != 0:
  48. time = timestamp - inj_timestamp
  49. packets_so_far = time / inj_pps
  50. packets_this_second = packets_so_far % inj_pps
  51. else:
  52. inj_pps = 0
  53. if delay == 0:
  54. # Calculate request timestamp
  55. # To imitate the bursty behavior of traffic
  56. random_delay = lea.Lea.fromValFreqsDict({1 / pps: 70, 2 / pps: 20, 5 / pps: 7, 10 / pps: 3})
  57. result_delay = rnd.uniform(1 / pps, random_delay.random())
  58. else:
  59. # Calculate reply timestamp
  60. random_delay = lea.Lea.fromValFreqsDict({delay / 2: 70, delay / 3: 20, delay / 5: 7, delay / 10: 3})
  61. result_delay = rnd.uniform(1 / pps + delay, 1 / pps + random_delay.random())
  62. result = timestamp + result_delay
  63. if inj_pps > packets_this_second and int(result) - int(timestamp) != 1:
  64. result = result + 1
  65. return result
  66. def get_timestamp_from_datetime_str(time: str):
  67. return pytz.timezone('UTC').localize(dt.datetime.strptime(time, "%Y-%m-%d %H:%M:%S.%f")).timestamp()
  68. def get_interval_pps(complement_interval_pps, timestamp):
  69. """
  70. Gets the packet rate (pps) for a specific time interval.
  71. :param complement_interval_pps: an array of tuples (the last timestamp in the interval, the packet rate in the
  72. corresponding interval).
  73. :param timestamp: the timestamp at which the packet rate is required.
  74. :return: the corresponding packet rate (pps) .
  75. """
  76. for row in complement_interval_pps:
  77. if timestamp <= row[0]:
  78. return row[1]
  79. return complement_interval_pps[-1][1] # in case the timestamp > capture max timestamp
  80. def get_nth_random_element(*element_list):
  81. """
  82. Returns the n-th element of every list from an arbitrary number of given lists.
  83. For example, list1 contains IP addresses, list 2 contains MAC addresses. Use of this function ensures that
  84. the n-th IP address uses always the n-th MAC address.
  85. :param element_list: An arbitrary number of lists.
  86. :return: A tuple of the n-th element of every list.
  87. """
  88. if len(element_list) <= 0:
  89. return None
  90. elif len(element_list) == 1 and len(element_list[0]) > 0:
  91. return rnd.choice(element_list[0])
  92. else:
  93. range_max = min([len(x) for x in element_list])
  94. if range_max > 0:
  95. range_max -= 1
  96. n = rnd.randint(0, range_max)
  97. return tuple(x[n] for x in element_list)
  98. else:
  99. return None
  100. def get_rnd_os():
  101. """
  102. Chooses random platform over an operating system probability distribution
  103. :return: random platform as string
  104. """
  105. os_dist = lea.Lea.fromValFreqsDict(platform_probability)
  106. return os_dist.random()
  107. def check_platform(platform: str) -> None:
  108. """
  109. Checks if the given platform is currently supported
  110. if not exits with error
  111. :param platform: the platform, which should be validated
  112. """
  113. if platform not in platforms:
  114. raise ValueError("ERROR: Invalid platform: " + platform + "." +
  115. "\n Please select one of the following platforms: " + ",".join(platforms))
  116. def get_ip_range(start_ip: str, end_ip: str):
  117. """
  118. Generates a list of IPs of a given range. If the start_ip is greater than the end_ip, the reverse range is generated
  119. :param start_ip: the start_ip of the desired IP-range
  120. :param end_ip: the end_ip of the desired IP-range
  121. :return: a list of all IPs in the desired IP-range, including start-/end_ip
  122. """
  123. start = ipaddress.ip_address(start_ip)
  124. end = ipaddress.ip_address(end_ip)
  125. ips = []
  126. if start < end:
  127. while start <= end:
  128. ips.append(start.exploded)
  129. start = start + 1
  130. elif start > end:
  131. while start >= end:
  132. ips.append(start.exploded)
  133. start = start - 1
  134. else:
  135. ips.append(start_ip)
  136. return ips
  137. def generate_source_port_from_platform(platform: str, previous_port=0):
  138. """
  139. Generates the next source port according to the TCP-port-selection strategy of the given platform
  140. :param platform: the platform for which to generate source ports
  141. :param previous_port: the previously used/generated source port. Must be 0 if no port was generated before
  142. :return: the next source port for the given platform
  143. """
  144. check_platform(platform)
  145. if platform in {"winnt", "winxp", "win2000"}:
  146. if (previous_port == 0) or (previous_port + 1 > 5000):
  147. return rnd.randint(1024, 5000)
  148. else:
  149. return previous_port + 1
  150. elif platform == "linux":
  151. return rnd.randint(32768, 61000)
  152. else:
  153. if (previous_port == 0) or (previous_port + 1 > 65535):
  154. return rnd.randint(49152, 65535)
  155. else:
  156. return previous_port + 1
  157. def get_filetime_format(timestamp):
  158. """
  159. Converts a timestamp into MS FILETIME format
  160. :param timestamp: a timestamp in seconds
  161. :return: MS FILETIME timestamp
  162. """
  163. boot_datetime = dt.datetime.fromtimestamp(timestamp).astimezone(pytz.timezone('UTC'))
  164. boot_filetime = 116444736000000000 + (cal.timegm(boot_datetime.timetuple()) * 10000000)
  165. return boot_filetime + (boot_datetime.microsecond * 10)
  166. def get_rnd_boot_time(timestamp, platform="winxp"):
  167. """
  168. Generates a random boot time based on a given timestamp and operating system
  169. :param timestamp: a timestamp in seconds
  170. :param platform: a platform as string as specified in check_platform above. default is winxp. this param is optional
  171. :return: timestamp of random boot time in seconds since EPOCH
  172. """
  173. check_platform(platform)
  174. if platform is "linux":
  175. uptime_in_days = lea.Lea.fromValFreqsDict({3: 50, 7: 25, 14: 12.5, 31: 6.25, 92: 3.125, 183: 1.5625,
  176. 365: 0.78125, 1461: 0.390625, 2922: 0.390625})
  177. elif platform is "macos":
  178. uptime_in_days = lea.Lea.fromValFreqsDict({7: 50, 14: 25, 31: 12.5, 92: 6.25, 183: 3.125, 365: 3.076171875,
  179. 1461: 0.048828125})
  180. else:
  181. uptime_in_days = lea.Lea.fromValFreqsDict({3: 50, 7: 25, 14: 12.5, 31: 6.25, 92: 3.125, 183: 1.5625,
  182. 365: 0.78125, 1461: 0.78125})
  183. timestamp -= rnd.randint(0, uptime_in_days.random() * 86400)
  184. return timestamp
  185. def get_rnd_x86_nop(count=1, side_effect_free=False, char_filter=set()):
  186. """
  187. Generates a specified number of x86 single-byte (pseudo-)NOPs
  188. :param count: The number of bytes to generate
  189. :param side_effect_free: Determines whether NOPs with side-effects (to registers or the stack) are allowed
  190. :param char_filter: A set of bytes which are forbidden to generate
  191. :return: Random x86 NOP bytestring
  192. """
  193. result = b''
  194. nops = x86_nops.copy()
  195. if not side_effect_free:
  196. nops |= x86_pseudo_nops.copy()
  197. if not isinstance(char_filter, set):
  198. char_filter = set(char_filter)
  199. nops = list(nops - char_filter)
  200. for i in range(0, count):
  201. result += nops[rnd.randint(0, len(nops) - 1)]
  202. return result
  203. def get_rnd_bytes(count=1, ignore=None):
  204. """
  205. Generates a specified number of random bytes while excluding unwanted bytes
  206. :param count: Number of wanted bytes
  207. :param ignore: The bytes, which should be ignored, as an array
  208. :return: Random bytestring
  209. """
  210. if ignore is None:
  211. ignore = []
  212. result = b''
  213. for i in range(0, count):
  214. char = os.urandom(1)
  215. while char in ignore:
  216. char = os.urandom(1)
  217. result += char
  218. return result
  219. def check_payload_len(payload_len: int, limit: int) -> None:
  220. """
  221. Checks if the len of the payload exceeds a given limit
  222. :param payload_len: The length of the payload
  223. :param limit: The limit of the length of the payload which is allowed
  224. """
  225. if payload_len > limit:
  226. raise ValueError("Custom payload too long: " + str(payload_len) +
  227. " bytes. Should be a maximum of " + str(limit) + " bytes.")
  228. def get_bytes_from_file(filepath):
  229. """
  230. Converts the content of a file into its byte representation
  231. The content of the file can either be a string or hexadecimal numbers/bytes (e.g. shellcode)
  232. The file must have the keyword "str" or "hex" in its first line to specify the rest of the content
  233. If the content is hex, whitespaces, backslashes, "x", quotation marks and "+" are removed
  234. Example for a hexadecimal input file:
  235. hex
  236. "abcd ef \xff10\ff 'xaa' x \ ab"
  237. Output: b'\xab\xcd\xef\xff\x10\xff\xaa\xab'
  238. :param filepath: The path of the file from which to get the bytes
  239. :return: The bytes of the file (either a byte representation of a string or the bytes contained in the file)
  240. """
  241. try:
  242. file = open(filepath)
  243. result_bytes = b''
  244. header = file.readline().strip()
  245. content = file.read()
  246. if header == "hex":
  247. content = content.replace(" ", "").replace("\n", "").replace("\\", "").replace("x", "").replace("\"", "") \
  248. .replace("'", "").replace("+", "").replace("\r", "")
  249. try:
  250. result_bytes = bytes.fromhex(content)
  251. except ValueError:
  252. print("\nERROR: Content of file is not all hexadecimal.")
  253. file.close()
  254. exit(1)
  255. elif header == "str":
  256. result_bytes = content.strip().encode()
  257. else:
  258. print("\nERROR: Invalid header found: " + header + ". Try 'hex' or 'str' followed by endline instead.")
  259. file.close()
  260. exit(1)
  261. for forbidden_char in forbidden_chars:
  262. if forbidden_char in result_bytes:
  263. print("\nERROR: Forbidden character found in payload: ", forbidden_char)
  264. file.close()
  265. exit(1)
  266. file.close()
  267. return result_bytes
  268. except FileNotFoundError:
  269. print("\nERROR: File not found: ", filepath)
  270. exit(1)
  271. def handle_most_used_outputs(most_used_x):
  272. """
  273. :param most_used_x: Element or list (e.g. from SQL-query output) which should only be one element
  274. :return: most_used_x if it's not a list. The first element of most_used_x after being sorted if it's a list.
  275. None if that list is empty.
  276. """
  277. if isinstance(most_used_x, list):
  278. if len(most_used_x) == 0:
  279. return None
  280. most_used_x.sort()
  281. return most_used_x[0]
  282. else:
  283. return most_used_x
  284. def get_attacker_config(ip_source_list, ip_address: str):
  285. """
  286. Returns the attacker configuration depending on the IP address, this includes the port for the next
  287. attacking packet and the previously used (fixed) TTL value.
  288. :param ip_source_list: List of source IPs
  289. :param ip_address: The IP address of the attacker
  290. :return: A tuple consisting of (port, ttlValue)
  291. """
  292. # Gamma distribution parameters derived from MAWI 13.8G dataset
  293. alpha, loc, beta = (2.3261710235, -0.188306914406, 44.4853123884)
  294. gd = stats.gamma.rvs(alpha, loc=loc, scale=beta, size=len(ip_source_list))
  295. # Determine port
  296. port = attacker_port_mapping.get(ip_address)
  297. if port is not None: # use next port
  298. next_port = attacker_port_mapping.get(ip_address) + 1
  299. if next_port > (2 ** 16 - 1):
  300. next_port = 1
  301. else: # generate starting port
  302. next_port = inet.RandShort()
  303. attacker_port_mapping[ip_address] = next_port
  304. # Determine TTL value
  305. ttl = attacker_ttl_mapping.get(ip_address)
  306. if ttl is None: # determine TTL value
  307. is_invalid = True
  308. pos = ip_source_list.index(ip_address)
  309. pos_max = len(gd)
  310. while is_invalid:
  311. ttl = int(round(gd[pos]))
  312. if 0 < ttl < 256: # validity check
  313. is_invalid = False
  314. else:
  315. pos = (pos + 1) % pos_max
  316. attacker_ttl_mapping[ip_address] = ttl
  317. # return port and TTL
  318. return next_port, ttl
  319. def remove_generic_ending(string):
  320. """"
  321. Returns the input string with it's ending cut off, in case it was a generic one
  322. :param string: Input string
  323. :return: Input string with ending cut off
  324. """
  325. for end in generic_attack_names:
  326. if string.endswith(end):
  327. return string[:-len(end)]
  328. return string
  329. def get_botnet_pcap_db():
  330. """
  331. Reads a botnet resource pcap, calculates statistics for it and returns the DB path.
  332. :return: the database path for the botnet resource pcap statistics DB
  333. """
  334. import Core.Statistics
  335. import ID2TLib.PcapFile as PcapFile
  336. bot_pcap = PcapFile.PcapFile(BOTNET_PCAP)
  337. bot_stats = Core.Statistics.Statistics(bot_pcap)
  338. bot_stats.do_extra_tests = True
  339. bot_stats.load_pcap_statistics(False, False, True, True, [], False, False)
  340. return bot_pcap.get_db_path()