peutils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. """peutils, Portable Executable utilities module
  2. Copyright (c) 2005-2023 Ero Carrera <ero.carrera@gmail.com>
  3. All rights reserved.
  4. """
  5. import os
  6. import re
  7. import string
  8. import urllib.error
  9. import urllib.parse
  10. import urllib.request
  11. import pefile
  12. __author__ = "Ero Carrera"
  13. __version__ = pefile.__version__
  14. __contact__ = "ero.carrera@gmail.com"
  15. class SignatureDatabase:
  16. """This class loads and keeps a parsed PEiD signature database.
  17. Usage:
  18. sig_db = SignatureDatabase('/path/to/signature/file')
  19. and/or
  20. sig_db = SignatureDatabase()
  21. sig_db.load('/path/to/signature/file')
  22. Signature databases can be combined by performing multiple loads.
  23. The filename parameter can be a URL too. In that case the
  24. signature database will be downloaded from that location.
  25. """
  26. def __init__(self, filename=None, data=None):
  27. # RegExp to match a signature block
  28. #
  29. self.parse_sig = re.compile(
  30. r"\[(.*?)\]\s+?signature\s*=\s*(.*?)(\s+\?\?)*\s*ep_only\s*=\s*(\w+)(?:\s*section_start_only\s*=\s*(\w+)|)",
  31. re.S,
  32. )
  33. # Signature information
  34. #
  35. # Signatures are stored as trees using dictionaries
  36. # The keys are the byte values while the values for
  37. # each key are either:
  38. #
  39. # - Other dictionaries of the same form for further
  40. # bytes in the signature
  41. #
  42. # - A dictionary with a string as a key (packer name)
  43. # and None as value to indicate a full signature
  44. #
  45. self.signature_tree_eponly_true = dict()
  46. self.signature_count_eponly_true = 0
  47. self.signature_tree_eponly_false = dict()
  48. self.signature_count_eponly_false = 0
  49. self.signature_tree_section_start = dict()
  50. self.signature_count_section_start = 0
  51. # The depth (length) of the longest signature
  52. #
  53. self.max_depth = 0
  54. self.__load(filename=filename, data=data)
  55. def generate_section_signatures(self, pe, name, sig_length=512):
  56. """Generates signatures for all the sections in a PE file.
  57. If the section contains any data a signature will be created
  58. for it. The signature name will be a combination of the
  59. parameter 'name' and the section number and its name.
  60. """
  61. section_signatures = list()
  62. for idx, section in enumerate(pe.sections):
  63. if section.SizeOfRawData < sig_length:
  64. continue
  65. # offset = pe.get_offset_from_rva(section.VirtualAddress)
  66. offset = section.PointerToRawData
  67. sig_name = "%s Section(%d/%d,%s)" % (
  68. name,
  69. idx + 1,
  70. len(pe.sections),
  71. "".join([c for c in section.Name if c in string.printable]),
  72. )
  73. section_signatures.append(
  74. self.__generate_signature(
  75. pe,
  76. offset,
  77. sig_name,
  78. ep_only=False,
  79. section_start_only=True,
  80. sig_length=sig_length,
  81. )
  82. )
  83. return "\n".join(section_signatures) + "\n"
  84. def generate_ep_signature(self, pe, name, sig_length=512):
  85. """Generate signatures for the entry point of a PE file.
  86. Creates a signature whose name will be the parameter 'name'
  87. and the section number and its name.
  88. """
  89. offset = pe.get_offset_from_rva(pe.OPTIONAL_HEADER.AddressOfEntryPoint)
  90. return self.__generate_signature(
  91. pe, offset, name, ep_only=True, sig_length=sig_length
  92. )
  93. def __generate_signature(
  94. self, pe, offset, name, ep_only=False, section_start_only=False, sig_length=512
  95. ):
  96. data = pe.__data__[offset : offset + sig_length]
  97. signature_bytes = " ".join(["%02x" % ord(c) for c in data])
  98. if ep_only == True:
  99. ep_only = "true"
  100. else:
  101. ep_only = "false"
  102. if section_start_only == True:
  103. section_start_only = "true"
  104. else:
  105. section_start_only = "false"
  106. signature = "[%s]\nsignature = %s\nep_only = %s\nsection_start_only = %s\n" % (
  107. name,
  108. signature_bytes,
  109. ep_only,
  110. section_start_only,
  111. )
  112. return signature
  113. def match(self, pe, ep_only=True, section_start_only=False):
  114. """Matches and returns the exact match(es).
  115. If ep_only is True the result will be a string with
  116. the packer name. Otherwise it will be a list of the
  117. form (file_offset, packer_name) specifying where
  118. in the file the signature was found.
  119. """
  120. matches = self.__match(pe, ep_only, section_start_only)
  121. # The last match (the most precise) from the
  122. # list of matches (if any) is returned
  123. #
  124. if matches:
  125. if ep_only == False:
  126. # Get the most exact match for each list of matches
  127. # at a given offset
  128. #
  129. return [(match[0], match[1][-1]) for match in matches]
  130. return matches[1][-1]
  131. return None
  132. def match_all(self, pe, ep_only=True, section_start_only=False):
  133. """Matches and returns all the likely matches."""
  134. matches = self.__match(pe, ep_only, section_start_only)
  135. if matches:
  136. if ep_only == False:
  137. # Get the most exact match for each list of matches
  138. # at a given offset
  139. #
  140. return matches
  141. return matches[1]
  142. return None
  143. def __match(self, pe, ep_only, section_start_only):
  144. # Load the corresponding set of signatures
  145. # Either the one for ep_only equal to True or
  146. # to False
  147. #
  148. if section_start_only is True:
  149. # Fetch the data of the executable as it'd
  150. # look once loaded in memory
  151. #
  152. try:
  153. data = pe.__data__
  154. except Exception as excp:
  155. raise
  156. # Load the corresponding tree of signatures
  157. #
  158. signatures = self.signature_tree_section_start
  159. # Set the starting address to start scanning from
  160. #
  161. scan_addresses = [section.PointerToRawData for section in pe.sections]
  162. elif ep_only is True:
  163. # Fetch the data of the executable as it'd
  164. # look once loaded in memory
  165. #
  166. try:
  167. data = pe.get_memory_mapped_image()
  168. except Exception as excp:
  169. raise
  170. # Load the corresponding tree of signatures
  171. #
  172. signatures = self.signature_tree_eponly_true
  173. # Fetch the entry point of the PE file and the data
  174. # at the entry point
  175. #
  176. ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
  177. # Set the starting address to start scanning from
  178. #
  179. scan_addresses = [ep]
  180. else:
  181. data = pe.__data__
  182. signatures = self.signature_tree_eponly_false
  183. scan_addresses = range(len(data))
  184. # For each start address, check if any signature matches
  185. #
  186. matches = []
  187. for idx in scan_addresses:
  188. result = self.__match_signature_tree(
  189. signatures, data[idx : idx + self.max_depth]
  190. )
  191. if result:
  192. matches.append((idx, result))
  193. # Return only the matched items found at the entry point if
  194. # ep_only is True (matches will have only one element in that
  195. # case)
  196. #
  197. if ep_only is True:
  198. if matches:
  199. return matches[0]
  200. return matches
  201. def match_data(self, code_data, ep_only=True, section_start_only=False):
  202. data = code_data
  203. scan_addresses = [0]
  204. # Load the corresponding set of signatures
  205. # Either the one for ep_only equal to True or
  206. # to False
  207. #
  208. if section_start_only is True:
  209. # Load the corresponding tree of signatures
  210. #
  211. signatures = self.signature_tree_section_start
  212. # Set the starting address to start scanning from
  213. #
  214. elif ep_only is True:
  215. # Load the corresponding tree of signatures
  216. #
  217. signatures = self.signature_tree_eponly_true
  218. # For each start address, check if any signature matches
  219. #
  220. matches = []
  221. for idx in scan_addresses:
  222. result = self.__match_signature_tree(
  223. signatures, data[idx : idx + self.max_depth]
  224. )
  225. if result:
  226. matches.append((idx, result))
  227. # Return only the matched items found at the entry point if
  228. # ep_only is True (matches will have only one element in that
  229. # case)
  230. #
  231. if ep_only is True:
  232. if matches:
  233. return matches[0]
  234. return matches
  235. def __match_signature_tree(self, signature_tree, data, depth=0):
  236. """Recursive function to find matches along the signature tree.
  237. signature_tree is the part of the tree left to walk
  238. data is the data being checked against the signature tree
  239. depth keeps track of how far we have gone down the tree
  240. """
  241. matched_names = list()
  242. match = signature_tree
  243. # Walk the bytes in the data and match them
  244. # against the signature
  245. #
  246. for idx, byte in enumerate([b if isinstance(b, int) else ord(b) for b in data]):
  247. # If the tree is exhausted...
  248. #
  249. if match is None:
  250. break
  251. # Get the next byte in the tree
  252. #
  253. match_next = match.get(byte, None)
  254. # If None is among the values for the key
  255. # it means that a signature in the database
  256. # ends here and that there's an exact match.
  257. #
  258. if None in list(match.values()):
  259. # idx represent how deep we are in the tree
  260. #
  261. # names = [idx+depth]
  262. names = list()
  263. # For each of the item pairs we check
  264. # if it has an element other than None,
  265. # if not then we have an exact signature
  266. #
  267. for item in list(match.items()):
  268. if item[1] is None:
  269. names.append(item[0])
  270. matched_names.append(names)
  271. # If a wildcard is found keep scanning the signature
  272. # ignoring the byte.
  273. #
  274. if "??" in match:
  275. match_tree_alternate = match.get("??", None)
  276. data_remaining = data[idx + 1 :]
  277. if data_remaining:
  278. matched_names.extend(
  279. self.__match_signature_tree(
  280. match_tree_alternate, data_remaining, idx + depth + 1
  281. )
  282. )
  283. match = match_next
  284. # If we have any more packer name in the end of the signature tree
  285. # add them to the matches
  286. #
  287. if match is not None and None in list(match.values()):
  288. # names = [idx + depth + 1]
  289. names = list()
  290. for item in list(match.items()):
  291. if item[1] is None:
  292. names.append(item[0])
  293. matched_names.append(names)
  294. return matched_names
  295. def load(self, filename=None, data=None):
  296. """Load a PEiD signature file.
  297. Invoking this method on different files combines the signatures.
  298. """
  299. self.__load(filename=filename, data=data)
  300. def __load(self, filename=None, data=None):
  301. if filename is not None:
  302. # If the path does not exist, attempt to open a URL
  303. #
  304. if not os.path.exists(filename):
  305. try:
  306. sig_f = urllib.request.urlopen(filename)
  307. sig_data = sig_f.read()
  308. sig_f.close()
  309. except OSError:
  310. # Let this be raised back to the user...
  311. raise
  312. else:
  313. # Get the data for a file
  314. #
  315. try:
  316. sig_f = open(filename, "rt")
  317. sig_data = sig_f.read()
  318. sig_f.close()
  319. except OSError:
  320. # Let this be raised back to the user...
  321. raise
  322. else:
  323. sig_data = data
  324. # If the file/URL could not be read or no "raw" data
  325. # was provided there's nothing else to do
  326. #
  327. if not sig_data:
  328. return
  329. # Helper function to parse the signature bytes
  330. #
  331. def to_byte(value):
  332. if "?" in value:
  333. return value
  334. return int(value, 16)
  335. # Parse all the signatures in the file
  336. #
  337. matches = self.parse_sig.findall(sig_data)
  338. # For each signature, get the details and load it into the
  339. # signature tree
  340. #
  341. for (
  342. packer_name,
  343. signature,
  344. superfluous_wildcards,
  345. ep_only,
  346. section_start_only,
  347. ) in matches:
  348. ep_only = ep_only.strip().lower()
  349. signature = signature.replace("\\n", "").strip()
  350. signature_bytes = [to_byte(b) for b in signature.split()]
  351. if ep_only == "true":
  352. ep_only = True
  353. else:
  354. ep_only = False
  355. if section_start_only == "true":
  356. section_start_only = True
  357. else:
  358. section_start_only = False
  359. depth = 0
  360. if section_start_only is True:
  361. tree = self.signature_tree_section_start
  362. self.signature_count_section_start += 1
  363. else:
  364. if ep_only is True:
  365. tree = self.signature_tree_eponly_true
  366. self.signature_count_eponly_true += 1
  367. else:
  368. tree = self.signature_tree_eponly_false
  369. self.signature_count_eponly_false += 1
  370. for idx, byte in enumerate(signature_bytes):
  371. if idx + 1 == len(signature_bytes):
  372. tree[byte] = tree.get(byte, dict())
  373. tree[byte][packer_name] = None
  374. else:
  375. tree[byte] = tree.get(byte, dict())
  376. tree = tree[byte]
  377. depth += 1
  378. if depth > self.max_depth:
  379. self.max_depth = depth
  380. def is_valid(pe):
  381. """"""
  382. pass
  383. def is_suspicious(pe):
  384. """
  385. unusual locations of import tables
  386. non recognized section names
  387. presence of long ASCII strings
  388. """
  389. relocations_overlap_entry_point = False
  390. sequential_relocs = 0
  391. # If relocation data is found and the entries go over the entry point, and also are very
  392. # continuous or point outside section's boundaries => it might imply that an obfuscation
  393. # trick is being used or the relocations are corrupt (maybe intentionally)
  394. #
  395. if hasattr(pe, "DIRECTORY_ENTRY_BASERELOC"):
  396. for base_reloc in pe.DIRECTORY_ENTRY_BASERELOC:
  397. last_reloc_rva = None
  398. for reloc in base_reloc.entries:
  399. if reloc.rva <= pe.OPTIONAL_HEADER.AddressOfEntryPoint <= reloc.rva + 4:
  400. relocations_overlap_entry_point = True
  401. if (
  402. last_reloc_rva is not None
  403. and last_reloc_rva <= reloc.rva <= last_reloc_rva + 4
  404. ):
  405. sequential_relocs += 1
  406. last_reloc_rva = reloc.rva
  407. # If import tables or strings exist (are pointed to) to within the header or in the area
  408. # between the PE header and the first section that's suspicious
  409. #
  410. # IMPLEMENT
  411. warnings_while_parsing = False
  412. # If we have warnings, that's suspicious, some of those will be because of out-of-ordinary
  413. # values are found in the PE header fields
  414. # Things that are reported in warnings:
  415. # (parsing problems, special section characteristics i.e. W & X, uncommon values of fields,
  416. # unusual entrypoint, suspicious imports)
  417. #
  418. warnings = pe.get_warnings()
  419. if warnings:
  420. warnings_while_parsing
  421. # If there are few or none (should come with a standard "density" of strings/kilobytes of data) longer (>8)
  422. # ascii sequences that might indicate packed data, (this is similar to the entropy test in some ways but
  423. # might help to discard cases of legitimate installer or compressed data)
  424. # If compressed data (high entropy) and is_driver => uuuuhhh, nasty
  425. pass
  426. def is_probably_packed(pe):
  427. """Returns True is there is a high likelihood that a file is packed or contains compressed data.
  428. The sections of the PE file will be analyzed, if enough sections
  429. look like containing compressed data and the data makes
  430. up for more than 20% of the total file size, the function will
  431. return True.
  432. """
  433. # Calculate the length of the data up to the end of the last section in the
  434. # file. Overlay data won't be taken into account
  435. #
  436. total_pe_data_length = len(pe.trim())
  437. # Assume that the file is packed when no data is available
  438. if not total_pe_data_length:
  439. return True
  440. has_significant_amount_of_compressed_data = False
  441. # If some of the sections have high entropy and they make for more than 20% of the file's size
  442. # it's assumed that it could be an installer or a packed file
  443. total_compressed_data = 0
  444. for section in pe.sections:
  445. s_entropy = section.get_entropy()
  446. s_length = len(section.get_data())
  447. # The value of 7.4 is empirical, based on looking at a few files packed
  448. # by different packers
  449. if s_entropy > 7.4:
  450. total_compressed_data += s_length
  451. if (total_compressed_data / total_pe_data_length) > 0.2:
  452. has_significant_amount_of_compressed_data = True
  453. return has_significant_amount_of_compressed_data