exeplot 0.2.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {exeplot-0.2.0 → exeplot-0.3.1}/.github/workflows/python-package.yml +33 -3
  2. {exeplot-0.2.0 → exeplot-0.3.1}/PKG-INFO +13 -7
  3. {exeplot-0.2.0 → exeplot-0.3.1}/README.md +1 -1
  4. {exeplot-0.2.0 → exeplot-0.3.1}/pyproject.toml +12 -4
  5. exeplot-0.3.1/src/exeplot/VERSION.txt +1 -0
  6. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__conf__.py +3 -0
  7. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__info__.py +6 -1
  8. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/__common__.py +14 -31
  9. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/__init__.py +3 -2
  10. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/byte.py +2 -1
  11. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/entropy.py +3 -5
  12. exeplot-0.3.1/src/exeplot/plots/graph.py +44 -0
  13. exeplot-0.2.0/src/exeplot/plots/nested-pie.py → exeplot-0.3.1/src/exeplot/plots/nested_pie.py +2 -1
  14. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/pie.py +2 -1
  15. exeplot-0.3.1/src/exeplot/utils.py +63 -0
  16. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/PKG-INFO +13 -7
  17. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/SOURCES.txt +3 -2
  18. exeplot-0.3.1/src/exeplot.egg-info/requires.txt +8 -0
  19. exeplot-0.3.1/tests/test_others.py +34 -0
  20. {exeplot-0.2.0 → exeplot-0.3.1}/tests/test_plots.py +9 -3
  21. exeplot-0.2.0/.github/workflows/pypi-publish.yml +0 -37
  22. exeplot-0.2.0/src/exeplot/VERSION.txt +0 -1
  23. exeplot-0.2.0/src/exeplot.egg-info/requires.txt +0 -2
  24. exeplot-0.2.0/tests/test_others.py +0 -17
  25. {exeplot-0.2.0 → exeplot-0.3.1}/.coveragerc +0 -0
  26. {exeplot-0.2.0 → exeplot-0.3.1}/.gitignore +0 -0
  27. {exeplot-0.2.0 → exeplot-0.3.1}/.readthedocs.yml +0 -0
  28. {exeplot-0.2.0 → exeplot-0.3.1}/LICENSE +0 -0
  29. {exeplot-0.2.0 → exeplot-0.3.1}/_config.yml +0 -0
  30. {exeplot-0.2.0 → exeplot-0.3.1}/docs/coverage.svg +0 -0
  31. {exeplot-0.2.0 → exeplot-0.3.1}/docs/mkdocs.yml +0 -0
  32. {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/css/extra.css +0 -0
  33. {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/img/icon.png +0 -0
  34. {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/img/logo.png +0 -0
  35. {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/index.md +0 -0
  36. {exeplot-0.2.0 → exeplot-0.3.1}/docs/requirements.txt +0 -0
  37. {exeplot-0.2.0 → exeplot-0.3.1}/pytest.ini +0 -0
  38. {exeplot-0.2.0 → exeplot-0.3.1}/requirements.txt +0 -0
  39. {exeplot-0.2.0 → exeplot-0.3.1}/setup.cfg +0 -0
  40. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__init__.py +0 -0
  41. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__main__.py +0 -0
  42. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/diff.py +0 -0
  43. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/dependency_links.txt +0 -0
  44. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/entry_points.txt +0 -0
  45. {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/top_level.txt +0 -0
  46. {exeplot-0.2.0 → exeplot-0.3.1}/tests/__init__.py +0 -0
  47. {exeplot-0.2.0 → exeplot-0.3.1}/tests/hello.elf +0 -0
  48. {exeplot-0.2.0 → exeplot-0.3.1}/tests/hello.exe +0 -0
  49. {exeplot-0.2.0 → exeplot-0.3.1}/tests/hello.macho +0 -0
@@ -29,7 +29,7 @@ jobs:
29
29
  - name: Install ${{ env.package }}
30
30
  run: |
31
31
  python -m pip install --upgrade pip
32
- python -m pip install flake8 pytest pytest-cov pytest-pythonpath coverage
32
+ python -m pip install flake8 pytest pytest-cov coverage
33
33
  pip install -r requirements.txt
34
34
  pip install .
35
35
  - name: Lint with flake8
@@ -51,7 +51,7 @@ jobs:
51
51
  - name: Install ${{ env.package }}
52
52
  run: |
53
53
  python -m pip install --upgrade pip
54
- python -m pip install pytest pytest-cov pytest-pythonpath
54
+ python -m pip install pytest pytest-cov
55
55
  pip install -r requirements.txt
56
56
  pip install .
57
57
  - name: Make coverage badge for ${{ env.package }}
@@ -60,7 +60,7 @@ jobs:
60
60
  pytest --cov=$package --cov-report=xml
61
61
  genbadge coverage -i coverage.xml -o $cov_badge_path
62
62
  - name: Verify Changed files
63
- uses: tj-actions/verify-changed-files@v17
63
+ uses: tj-actions/verify-changed-files@v12
64
64
  id: changed_files
65
65
  with:
66
66
  files: ${{ env.cov_badge_path }}
@@ -77,3 +77,33 @@ jobs:
77
77
  with:
78
78
  github_token: ${{ secrets.github_token }}
79
79
  branch: ${{ github.ref }}
80
+ deploy:
81
+ runs-on: ubuntu-latest
82
+ needs: coverage
83
+ steps:
84
+ - uses: actions/checkout@v3
85
+ with:
86
+ fetch-depth: 0
87
+ - name: Check for version change
88
+ uses: dorny/paths-filter@v2
89
+ id: filter
90
+ with:
91
+ filters: |
92
+ version:
93
+ - '**/VERSION.txt'
94
+ - if: steps.filter.outputs.version == 'true'
95
+ name: Cleanup README
96
+ run: |
97
+ sed -ri 's/^(##*)\s*:.*:\s*/\1 /g' README.md
98
+ awk '{if (match($0,"## Supporters")) exit; print}' README.md > README
99
+ mv -f README README.md
100
+ - if: steps.filter.outputs.version == 'true'
101
+ name: Build ${{ env.package }} package
102
+ run: python3 -m pip install --upgrade build && python3 -m build
103
+ - if: steps.filter.outputs.version == 'true'
104
+ name: Upload ${{ env.package }} to PyPi
105
+ uses: pypa/gh-action-pypi-publish@release/v1
106
+ with:
107
+ password: ${{ secrets.PYPI_API_TOKEN }}
108
+ verbose: true
109
+ verify_metadata: false
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: exeplot
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Library for plotting executable samples supporting multiple formats
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -679,10 +679,10 @@ License: GNU GENERAL PUBLIC LICENSE
679
679
  <https://www.gnu.org/licenses/why-not-lgpl.html>.
680
680
 
681
681
  Project-URL: documentation, https://python-exeplot.readthedocs.io/en/latest/?badge=latest
682
- Project-URL: homepage, https://github.com/dhondta/python-exeplot
683
- Project-URL: issues, https://github.com/dhondta/python-exeplot/issues
684
- Project-URL: repository, https://github.com/dhondta/python-exeplot
685
- Keywords: python,development,programming,executable-samples,plot
682
+ Project-URL: homepage, https://github.com/packing-box/python-exeplot
683
+ Project-URL: issues, https://github.com/packing-box/python-exeplot/issues
684
+ Project-URL: repository, https://github.com/packing-box/python-exeplot
685
+ Keywords: python,development,programming,executable-samples,plot,entropy,cfg
686
686
  Classifier: Development Status :: 5 - Production/Stable
687
687
  Classifier: Environment :: Console
688
688
  Classifier: Intended Audience :: Developers
@@ -694,8 +694,14 @@ Description-Content-Type: text/markdown
694
694
  License-File: LICENSE
695
695
  Requires-Dist: lief>=0.16.1
696
696
  Requires-Dist: matplotlib
697
+ Provides-Extra: graph
698
+ Requires-Dist: angr>=9.2; extra == "graph"
699
+ Requires-Dist: networkx>=3.4.2; extra == "graph"
700
+ Requires-Dist: numpy<2; extra == "graph"
701
+ Requires-Dist: pygraphviz>=1.14; extra == "graph"
702
+ Dynamic: license-file
697
703
 
698
- <p align="center"><img src="https://github.com/packing-box/python-exeplot/raw/main/docs/pages/img/logo.png"></p>
704
+ <p align="center" id="top"><img src="https://github.com/packing-box/python-exeplot/raw/main/docs/pages/img/logo.png"></p>
699
705
  <h1 align="center">ExePlot <a href="https://twitter.com/intent/tweet?text=ExePlot%20-%20Plot%20executable%20samples%20easy.%0D%0ALibrary%20for%20plotting%20executable%20samples%20supporting%20multiple%20formats.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fpacking-box%2fpython-exeplot%0D%0A&hashtags=python,programming,executable-samples,plot"><img src="https://img.shields.io/badge/Tweet--lightgrey?logo=twitter&style=social" alt="Tweet" height="20"/></a></h1>
700
706
  <h3 align="center">Search for samples from various malware databases.</h3>
701
707
 
@@ -1,4 +1,4 @@
1
- <p align="center"><img src="https://github.com/packing-box/python-exeplot/raw/main/docs/pages/img/logo.png"></p>
1
+ <p align="center" id="top"><img src="https://github.com/packing-box/python-exeplot/raw/main/docs/pages/img/logo.png"></p>
2
2
  <h1 align="center">ExePlot <a href="https://twitter.com/intent/tweet?text=ExePlot%20-%20Plot%20executable%20samples%20easy.%0D%0ALibrary%20for%20plotting%20executable%20samples%20supporting%20multiple%20formats.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fpacking-box%2fpython-exeplot%0D%0A&hashtags=python,programming,executable-samples,plot"><img src="https://img.shields.io/badge/Tweet--lightgrey?logo=twitter&style=social" alt="Tweet" height="20"/></a></h1>
3
3
  <h3 align="center">Search for samples from various malware databases.</h3>
4
4
 
@@ -21,7 +21,7 @@ authors = [
21
21
  ]
22
22
  description = "Library for plotting executable samples supporting multiple formats"
23
23
  license = {file = "LICENSE"}
24
- keywords = ["python", "development", "programming", "executable-samples", "plot"]
24
+ keywords = ["python", "development", "programming", "executable-samples", "plot", "entropy", "cfg"]
25
25
  requires-python = ">=3.9,<4"
26
26
  classifiers = [
27
27
  "Development Status :: 5 - Production/Stable",
@@ -37,15 +37,23 @@ dependencies = [
37
37
  ]
38
38
  dynamic = ["version"]
39
39
 
40
+ [project.optional-dependencies]
41
+ graph = [
42
+ "angr>=9.2",
43
+ "networkx>=3.4.2",
44
+ "numpy<2", # required until angr gets compatible with numpy>=2
45
+ "pygraphviz>=1.14",
46
+ ]
47
+
40
48
  [project.readme]
41
49
  file = "README.md"
42
50
  content-type = "text/markdown"
43
51
 
44
52
  [project.urls]
45
53
  documentation = "https://python-exeplot.readthedocs.io/en/latest/?badge=latest"
46
- homepage = "https://github.com/dhondta/python-exeplot"
47
- issues = "https://github.com/dhondta/python-exeplot/issues"
48
- repository = "https://github.com/dhondta/python-exeplot"
54
+ homepage = "https://github.com/packing-box/python-exeplot"
55
+ issues = "https://github.com/packing-box/python-exeplot/issues"
56
+ repository = "https://github.com/packing-box/python-exeplot"
49
57
 
50
58
  [project.scripts]
51
59
  exeplot = "exeplot.__main__:main"
@@ -0,0 +1 @@
1
+ 0.3.1
@@ -1,6 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
2
  import logging
3
3
  import matplotlib.pyplot as plt
4
+ import numpy
4
5
  from functools import wraps
5
6
 
6
7
 
@@ -18,6 +19,8 @@ config = {
18
19
  'transparent': False,
19
20
  }
20
21
 
22
+ numpy.int = numpy.int_ # dirty fix to "AttributeError: module 'numpy' has no attribute 'int'."
23
+
21
24
 
22
25
  def configure(): # pragma: no cover
23
26
  from configparser import ConfigParser
@@ -3,12 +3,17 @@
3
3
 
4
4
  """
5
5
  import os
6
+ from datetime import datetime
7
+
8
+ __y = str(datetime.now().year)
9
+ __s = "2025"
6
10
 
7
11
  __author__ = "Alexandre D'Hondt"
8
- __copyright__ = "© 2025 A. D'Hondt"
12
+ __copyright__ = "© {} A. D'Hondt".format([__y, __s + "-" + __y][__y != __s])
9
13
  __email__ = "alexandre.dhondt@gmail.com"
10
14
  __license__ = "GPLv3 (https://www.gnu.org/licenses/gpl-3.0.fr.html)"
11
15
  __source__ = "https://github.com/packing-box/python-exeplot"
12
16
 
13
17
  with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
14
18
  __version__ = f.read().strip()
19
+
@@ -3,6 +3,8 @@ import os
3
3
  from functools import cached_property
4
4
  from statistics import mean
5
5
 
6
+ from ..utils import *
7
+
6
8
 
7
9
  CACHE_DIR = os.path.expanduser("~/.exeplot")
8
10
  # https://matplotlib.org/2.0.2/examples/color/named_colors.html
@@ -48,36 +50,17 @@ N_SAMPLES = 2048
48
50
  SHADOW = {'shade': .3, 'ox': .005, 'oy': -.005, 'linewidth': 0.}
49
51
  SUBLABELS = {
50
52
  'ep': lambda d: "EP at 0x%.8x in %s" % d['ep'][1:],
51
- 'size': lambda d: "Size = %s" % _human_readable_size(d['size'], 1),
53
+ 'size': lambda d: "Size = %s" % human_readable_size(d['size'], 1),
52
54
  'size-ep': lambda d: "Size = %s\nEP at 0x%.8x in %s" % \
53
- (_human_readable_size(d['size'], 1), d['ep'][1], d['ep'][2]),
55
+ (human_readable_size(d['size'], 1), d['ep'][1], d['ep'][2]),
54
56
  'size-ent': lambda d: "Size = %s\nAverage entropy: %.2f\nOverall entropy: %.2f" % \
55
- (_human_readable_size(d['size'], 1), mean(d['entropy']) * 8, d['entropy*']),
57
+ (human_readable_size(d['size'], 1), mean(d['entropy']) * 8, d['entropy*']),
56
58
  'size-ep-ent': lambda d: "Size = %s\nEP at 0x%.8x in %s\nAverage entropy: %.2f\nOverall entropy: %.2f" % \
57
- (_human_readable_size(d['size'], 1), d['ep'][1], d['ep'][2], mean(d['entropy']) * 8,
59
+ (human_readable_size(d['size'], 1), d['ep'][1], d['ep'][2], mean(d['entropy']) * 8,
58
60
  d['entropy*']),
59
61
  }
60
62
 
61
63
 
62
- def _ensure_str(s, encoding='utf-8', errors='strict'):
63
- if isinstance(s, bytes):
64
- try:
65
- return s.decode(encoding, errors)
66
- except:
67
- return s.decode("latin-1")
68
- elif not isinstance(s, (str, bytes)):
69
- raise TypeError("not expecting type '%s'" % type(s))
70
- return s
71
-
72
-
73
- def _human_readable_size(size, precision=0):
74
- i, units = 0, ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
75
- while size >= 1024 and i < len(units)-1:
76
- i += 1
77
- size /= 1024.0
78
- return "%.*f%s" % (precision, size, units[i])
79
-
80
-
81
64
  class Binary:
82
65
  def __init__(self, path, **kwargs):
83
66
  from lief import logging, parse
@@ -132,7 +115,7 @@ class Binary:
132
115
  h_len = b.header.header_size + b.header.program_header_size * b.header.numberof_segments
133
116
  elif self.type == "MachO":
134
117
  h_len = [28, 32][str(b.header.magic)[-3:] == "_64"] + b.header.sizeof_cmds
135
- yield 0, f"[0] Header ({_human_readable_size(h_len)})", 0, h_len, "black"
118
+ yield 0, f"[0] Header ({human_readable_size(h_len)})", 0, h_len, "black"
136
119
  # then handle binary's sections
137
120
  color_cursor, i = 0, 1
138
121
  for section in sorted(b.sections, key=lambda s: s.offset):
@@ -145,18 +128,18 @@ class Binary:
145
128
  c = co[color_cursor % len(co)]
146
129
  color_cursor += 1
147
130
  start, end = section.offset, section.offset + section.size
148
- yield i, f"[{i}] {self.section_names[section.name]} ({_human_readable_size(end - start)})", start, end, c
131
+ yield i, f"[{i}] {self.section_names[section.name]} ({human_readable_size(end - start)})", start, end, c
149
132
  i += 1
150
133
  # sections header at the end for ELF files
151
134
  if self.type == "ELF":
152
135
  start, end = end, end + b.header.section_header_size * b.header.numberof_sections
153
- yield i, f"[{i}] Section Header ({_human_readable_size(end - start)})", start, end, "black"
136
+ yield i, f"[{i}] Section Header ({human_readable_size(end - start)})", start, end, "black"
154
137
  i += 1
155
138
  # finally, handle the overlay
156
139
  start, end = self.size - b.overlay.nbytes, self.size
157
- yield i, f"[{i}] Overlay ({_human_readable_size(end - start)})", start, self.size, "lightgray"
140
+ yield i, f"[{i}] Overlay ({human_readable_size(end - start)})", start, self.size, "lightgray"
158
141
  i += 1
159
- yield i, f"TOTAL: {_human_readable_size(self.size)}", None, None, "white"
142
+ yield i, f"TOTAL: {human_readable_size(self.size)}", None, None, "white"
160
143
 
161
144
  def __segments_data(self):
162
145
  b = self.__binary
@@ -164,11 +147,11 @@ class Binary:
164
147
  return # segments only apply to ELF and MachO
165
148
  elif self.type == "ELF":
166
149
  for i, s in enumerate(sorted(b.segments, key=lambda x: (x.file_offset, x.physical_size))):
167
- yield i, f"[{i}] {str(s.type).split('.')[1]} ({_human_readable_size(s.physical_size)})", \
150
+ yield i, f"[{i}] {str(s.type).split('.')[1]} ({human_readable_size(s.physical_size)})", \
168
151
  s.file_offset, s.file_offset+s.physical_size, "lightgray"
169
152
  elif self.type == "MachO":
170
153
  for i, s in enumerate(sorted(b.segments, key=lambda x: (x.file_offset, x.file_size))):
171
- yield i, f"[{i}] {s.name} ({_human_readable_size(s.file_size)})", \
154
+ yield i, f"[{i}] {s.name} ({human_readable_size(s.file_size)})", \
172
155
  s.file_offset, s.file_offset+s.file_size, "lightgray"
173
156
 
174
157
  def _data(self, segments=False, overlap=False):
@@ -255,7 +238,7 @@ class Binary:
255
238
 
256
239
  @cached_property
257
240
  def section_names(self):
258
- names = {s.name: _ensure_str(s.name).strip("\x00") or "<empty>" for s in self.__binary.sections}
241
+ names = {s.name: ensure_str(s.name).strip("\x00") or "<empty>" for s in self.__binary.sections}
259
242
  # names from string table only applies to PE
260
243
  if self.type != "PE":
261
244
  return names
@@ -6,13 +6,14 @@ import os
6
6
  __all__ = []
7
7
 
8
8
 
9
- for f in os.listdir(os.path.dirname(os.path.abspath(__file__))):
9
+ for f in sorted(os.listdir(os.path.dirname(os.path.abspath(__file__)))):
10
10
  if not f.endswith(".py") or f.startswith("_"):
11
11
  continue
12
12
  name = f[:-3]
13
13
  module = importlib.import_module(f".{name}", package=__name__)
14
- if hasattr(module, "plot") and callable(getattr(module, "plot")):
14
+ if getattr(module, "_IMP", True) and hasattr(module, "plot") and callable(getattr(module, "plot")):
15
15
  globals()[f"{name}"] = f = getattr(module, "plot")
16
16
  f.__args__ = getattr(module, "arguments")
17
+ f.__name__ = name
17
18
  __all__.append(name)
18
19
 
@@ -1,6 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
- from .__common__ import _human_readable_size, Binary, COLORS
2
+ from .__common__ import Binary, COLORS
3
3
  from ..__conf__ import save_figure
4
+ from ..utils import human_readable_size
4
5
 
5
6
 
6
7
  def arguments(parser):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
- from math import log2
3
-
4
2
  from .__common__ import mean, Binary, COLORS, MIN_ZONE_WIDTH, N_SAMPLES, SUBLABELS
5
3
  from ..__conf__ import save_figure
4
+ from ..utils import shannon_entropy
6
5
 
7
6
 
8
7
  def arguments(parser):
@@ -26,12 +25,11 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
26
25
  :param n_samples: number of samples of entropy required
27
26
  :param window_size: window size for computing the entropy
28
27
  """
29
- _entropy = lambda b: -sum([p*log2(p) for p in [float(ctr)/len(b) for ctr in [b.count(c) for c in set(b)]]]) or 0.
30
28
  binary = Binary(executable)
31
29
  data = {'hash': binary.hash, 'name': binary.basename, 'size': binary.size, 'type': binary.type,
32
30
  'entropy': [], 'sections': []}
33
31
  # compute window-based entropy
34
- data['entropy*'] = _entropy(binary.rawbytes)
32
+ data['entropy*'] = shannon_entropy(binary.rawbytes)
35
33
  step, cs = abs(binary.size // n_samples), binary.size / n_samples # chunk size
36
34
  if isinstance(window_size, type(lambda: 0)):
37
35
  window_size = window_size(step)
@@ -47,7 +45,7 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
47
45
  window += f.read(new_pos - cur_pos if i > 0 else winter)
48
46
  window = window[max(0, len(window)-window_size) if cur_pos + winter < binary.size else step:]
49
47
  # compute entropy
50
- data['entropy'].append(_entropy(window)/8.)
48
+ data['entropy'].append(shannon_entropy(window)/8.)
51
49
  # compute other characteristics using the Binary instance parsed with LIEF
52
50
  # convert to 3-tuple (EP offset on plot, EP file offset, section name containing EP)
53
51
  ep, ep_sec = binary.entrypoint, binary.entrypoint_section
@@ -0,0 +1,44 @@
1
+ # -*- coding: UTF-8 -*-
2
+ try:
3
+ import angr
4
+ import pygraphviz as pgv
5
+ import networkx as nx
6
+ _IMP = True
7
+ except ImportError:
8
+ _IMP = False
9
+ import matplotlib.pyplot as plt
10
+
11
+ from .__common__ import Binary, CACHE_DIR, COLORS, MIN_ZONE_WIDTH
12
+ from ..__conf__ import save_figure
13
+
14
+
15
+ _DEFAULT_ALGORITHM, _DEFAULT_ENGINE = "fast", "default"
16
+ _ENGINES = ["default", "pcode", "vex"]
17
+
18
+
19
+ def arguments(parser):
20
+ parser.add_argument("executable", help="executable sample to be plotted")
21
+ parser.add_argument("-a", "--algorithm", default=_DEFAULT_ALGORITHM, choices=["emulated", "fast"],
22
+ help="engine for CFG extraction by Angr")
23
+ parser.add_argument("-e", "--engine", default=_DEFAULT_ENGINE, choices=_ENGINES,
24
+ help="engine for CFG extraction by Angr")
25
+ return parser
26
+
27
+
28
+ @save_figure
29
+ def plot(executable, algorithm=_DEFAULT_ALGORITHM, engine=_DEFAULT_ENGINE, **kwargs):
30
+ """ plot the Control Flow Graph (CFG) of an executable """
31
+ from math import ceil, log2
32
+ engine = {k: getattr(angr.engines, "UberEngine" if k != "pcode" else f"UberEngine{k.capitalize()}") \
33
+ for k in _ENGINES}[engine]
34
+ project = angr.Project(executable, auto_load_libs=False, engine=engine)
35
+ cfg = getattr(project.analyses, f"CFG{algorithm.capitalize()}")()
36
+ labels, node_colors = {}, []
37
+ for node in cfg.graph.nodes():
38
+ labels[node] = f"{node.name}\n0x{node.addr:x}" if hasattr(node, "name") and node.name else f"0x{node.addr:x}"
39
+ node_colors.append("red" if node.function_address == node.addr else "lightblue")
40
+ n = max(10, min(30, ceil(log2(n_nodes := len(cfg.graph.nodes()) + 1) * 2)))
41
+ plt.figure(figsize=(n, n))
42
+ nx.draw(cfg.graph, nx.kamada_kawai_layout(cfg.graph), font_size=8, with_labels=True, labels=labels,
43
+ node_size=max(300, 15000 // n_nodes), node_color=node_colors)
44
+
@@ -1,6 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
- from .__common__ import _human_readable_size, Binary, COLORS, SHADOW
2
+ from .__common__ import Binary, COLORS, SHADOW
3
3
  from ..__conf__ import save_figure
4
+ from ..utils import human_readable_size
4
5
 
5
6
 
6
7
  def arguments(parser):
@@ -1,6 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
- from .__common__ import _human_readable_size, Binary, COLORS, SHADOW
2
+ from .__common__ import Binary, COLORS, SHADOW
3
3
  from ..__conf__ import save_figure
4
+ from ..utils import human_readable_size
4
5
 
5
6
 
6
7
  def arguments(parser):
@@ -0,0 +1,63 @@
1
+ # -*- coding: UTF-8 -*-
2
+ from math import log2
3
+
4
+
5
+ __all__ = ["ensure_str", "human_readable_size", "ngrams_counts", "ngrams_distribution", "shannon_entropy"]
6
+
7
+ shannon_entropy = lambda b: -sum([p*log2(p) for p in [float(ctr)/len(b) for ctr in [b.count(c) for c in set(b)]]]) or 0.
8
+
9
+
10
+ def ensure_str(s, encoding='utf-8', errors='strict'):
11
+ """ Ensure that an input string is decoded. """
12
+ if isinstance(s, bytes):
13
+ try:
14
+ return s.decode(encoding, errors)
15
+ except:
16
+ return s.decode("latin-1")
17
+ elif not isinstance(s, (str, bytes)):
18
+ raise TypeError("not expecting type '%s'" % type(s))
19
+ return s
20
+
21
+
22
+ def human_readable_size(size, precision=0):
23
+ """ Display bytes' size in a human-readable format given a precision. """
24
+ i, units = 0, ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
25
+ while size >= 1024 and i < len(units)-1:
26
+ i += 1
27
+ size /= 1024.0
28
+ return "%.*f%s" % (precision, size, units[i])
29
+
30
+
31
+ def ngrams_counts(byte_obj, n=1):
32
+ """ Output the Counter instance for an input byte sequence or byte object based on n-grams.
33
+ If the input is a byte object, cache the result.
34
+
35
+ :param n: n determining the size of n-grams, defaults to 1
36
+ """
37
+ from collections import Counter
38
+ if isinstance(byte_obj, (str, bytes)):
39
+ return Counter(byte_obj[i:i+n] for i in range(0, len(byte_obj) - n + 1))
40
+ elif hasattr(byte_obj, "bytes") and hasattr(byte_obj, "size"):
41
+ if not hasattr(byte_obj, "_ngram_counts_cache"):
42
+ byte_obj._ngram_counts_cache = {}
43
+ if n not in byte_obj._ngram_counts_cache.keys():
44
+ byte_obj._ngram_counts_cache[n] = Counter(byte_obj.bytes[i:i+n] for i in range(0, byte_obj.size - n + 1))
45
+ return byte_obj._ngram_counts_cache[n]
46
+ raise TypeError("Bad input type ; should be a byte sequence or object")
47
+
48
+
49
+ def ngrams_distribution(byte_obj, n=1, n_most_common=None, n_exclude_top=0, exclude=None):
50
+ """ Compute the n-grams distribution of an input byte sequence or byte object given exclusions.
51
+
52
+ :param n: n determining the size of n-grams, defaults to 1
53
+ :param n_most_common: number of n-grams to be kept in the result, keep all by default
54
+ :param n_exclude_top: number of n-grams to be excluded from the top of the histogram, no exclusion by default
55
+ :param exclude: list of specific n-grams to be excluded, no exclusion by default
56
+ :return: list of n_most_common (n-gram, count) pairs
57
+ """
58
+ c = ngrams_counts(byte_obj, n)
59
+ r = c.most_common(len(c) if n_most_common is None else n_most_common + n_exclude_top + len(exclude or []))
60
+ if exclude is not None:
61
+ r = [(ngram, count) for ngram, count in r if ngram not in exclude]
62
+ return r[n_exclude_top:n_exclude_top+(n_most_common or len(c))]
63
+
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: exeplot
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Library for plotting executable samples supporting multiple formats
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -679,10 +679,10 @@ License: GNU GENERAL PUBLIC LICENSE
679
679
  <https://www.gnu.org/licenses/why-not-lgpl.html>.
680
680
 
681
681
  Project-URL: documentation, https://python-exeplot.readthedocs.io/en/latest/?badge=latest
682
- Project-URL: homepage, https://github.com/dhondta/python-exeplot
683
- Project-URL: issues, https://github.com/dhondta/python-exeplot/issues
684
- Project-URL: repository, https://github.com/dhondta/python-exeplot
685
- Keywords: python,development,programming,executable-samples,plot
682
+ Project-URL: homepage, https://github.com/packing-box/python-exeplot
683
+ Project-URL: issues, https://github.com/packing-box/python-exeplot/issues
684
+ Project-URL: repository, https://github.com/packing-box/python-exeplot
685
+ Keywords: python,development,programming,executable-samples,plot,entropy,cfg
686
686
  Classifier: Development Status :: 5 - Production/Stable
687
687
  Classifier: Environment :: Console
688
688
  Classifier: Intended Audience :: Developers
@@ -694,8 +694,14 @@ Description-Content-Type: text/markdown
694
694
  License-File: LICENSE
695
695
  Requires-Dist: lief>=0.16.1
696
696
  Requires-Dist: matplotlib
697
+ Provides-Extra: graph
698
+ Requires-Dist: angr>=9.2; extra == "graph"
699
+ Requires-Dist: networkx>=3.4.2; extra == "graph"
700
+ Requires-Dist: numpy<2; extra == "graph"
701
+ Requires-Dist: pygraphviz>=1.14; extra == "graph"
702
+ Dynamic: license-file
697
703
 
698
- <p align="center"><img src="https://github.com/packing-box/python-exeplot/raw/main/docs/pages/img/logo.png"></p>
704
+ <p align="center" id="top"><img src="https://github.com/packing-box/python-exeplot/raw/main/docs/pages/img/logo.png"></p>
699
705
  <h1 align="center">ExePlot <a href="https://twitter.com/intent/tweet?text=ExePlot%20-%20Plot%20executable%20samples%20easy.%0D%0ALibrary%20for%20plotting%20executable%20samples%20supporting%20multiple%20formats.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fpacking-box%2fpython-exeplot%0D%0A&hashtags=python,programming,executable-samples,plot"><img src="https://img.shields.io/badge/Tweet--lightgrey?logo=twitter&style=social" alt="Tweet" height="20"/></a></h1>
700
706
  <h3 align="center">Search for samples from various malware databases.</h3>
701
707
 
@@ -7,7 +7,6 @@ _config.yml
7
7
  pyproject.toml
8
8
  pytest.ini
9
9
  requirements.txt
10
- .github/workflows/pypi-publish.yml
11
10
  .github/workflows/python-package.yml
12
11
  docs/coverage.svg
13
12
  docs/mkdocs.yml
@@ -21,6 +20,7 @@ src/exeplot/__conf__.py
21
20
  src/exeplot/__info__.py
22
21
  src/exeplot/__init__.py
23
22
  src/exeplot/__main__.py
23
+ src/exeplot/utils.py
24
24
  src/exeplot.egg-info/PKG-INFO
25
25
  src/exeplot.egg-info/SOURCES.txt
26
26
  src/exeplot.egg-info/dependency_links.txt
@@ -32,7 +32,8 @@ src/exeplot/plots/__init__.py
32
32
  src/exeplot/plots/byte.py
33
33
  src/exeplot/plots/diff.py
34
34
  src/exeplot/plots/entropy.py
35
- src/exeplot/plots/nested-pie.py
35
+ src/exeplot/plots/graph.py
36
+ src/exeplot/plots/nested_pie.py
36
37
  src/exeplot/plots/pie.py
37
38
  tests/__init__.py
38
39
  tests/hello.elf
@@ -0,0 +1,8 @@
1
+ lief>=0.16.1
2
+ matplotlib
3
+
4
+ [graph]
5
+ angr>=9.2
6
+ networkx>=3.4.2
7
+ numpy<2
8
+ pygraphviz>=1.14
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: UTF-8 -*-
3
+ import matplotlib.pyplot as plt
4
+ import os
5
+ from collections import Counter
6
+ from exeplot.plots.__common__ import Binary
7
+ from exeplot.utils import *
8
+ from unittest import TestCase
9
+
10
+
11
+ class TestOthers(TestCase):
12
+ def test_miscellaneous(self):
13
+ self.assertRaises(TypeError, ensure_str, 1)
14
+ for i in range(256):
15
+ self.assertIsNotNone(ensure_str(bytes([i])))
16
+ self.assertRaises(TypeError, Binary, "BAD")
17
+ binary = Binary(os.path.join(os.path.dirname(__file__), "hello.exe"))
18
+ self.assertIsNotNone(str(binary))
19
+
20
+
21
+ class TestUtils(TestCase):
22
+ def test_ngrams_functions(self):
23
+ self.assertTrue(isinstance(ngrams_counts(seq := b"\x00" * 4 + os.urandom(120) + b"\xff" * 4), Counter))
24
+ class Test:
25
+ bytes = seq
26
+ size = len(seq)
27
+ histogram = ngrams_distribution(t := Test(), exclude=(b"\x00", b"\xff"))
28
+ self.assertTrue(isinstance(histogram, list))
29
+ self.assertNotIn(b"\x00", [b for b, c in histogram])
30
+ self.assertNotIn(b"\xff", [b for b, c in histogram])
31
+ histogram2 = ngrams_distribution(t, n_most_common=300)
32
+ self.assertIn(b"\x00", [b for b, c in histogram2])
33
+ self.assertIn(b"\xff", [b for b, c in histogram2])
34
+
@@ -56,12 +56,18 @@ class TestPlotOptions(TestCase):
56
56
  for path in iter_files():
57
57
  print(f"plotting entropy of {path} (sublabel='size-ep-ent',scale=True,target='test.exe')...")
58
58
  entropy(path, sublabel="size-ep-ent", scale=True, target="test")
59
- print(f"plotting entropy of {path}.exe and {path}.elf (labels=['PE', lambda x:'ELF'],sublabel='size-ep-ent',scale=True)...")
59
+ print(f"plotting entropy of {path}.exe and {path}.elf (labels=['PE', lambda x:'ELF'],sublabel='size-ep-ent',"
60
+ "scale=True)...")
60
61
  path = os.path.join(os.path.dirname(__file__), "hello")
61
- entropy(f"{path}.exe", f"{path}.elf", labels=["PE", lambda x: "ELF"], sublabel="size-ep-ent", scale=True)
62
+ for img in entropy(f"{path}.exe", f"{path}.elf", labels=["PE", lambda x: "ELF"], sublabel="size-ep-ent",
63
+ scale=True):
64
+ os.remove(img)
65
+ plt.clf()
62
66
 
63
67
  def test_pie_plot_function(self):
64
68
  for path in iter_files():
65
69
  print(f"plotting pie of {path} (donut=True)...")
66
- pie(path, donut=True)
70
+ for img in pie(path, donut=True):
71
+ os.remove(img)
72
+ plt.clf()
67
73
 
@@ -1,37 +0,0 @@
1
- # This workflow will deploy the Python package to PyPi.org
2
-
3
- name: deploy
4
-
5
- env:
6
- package: exeplot
7
-
8
- on:
9
- push:
10
- branches:
11
- - main
12
- paths:
13
- - '**/VERSION.txt'
14
- workflow_run:
15
- workflows: ["build"]
16
- types: [completed]
17
-
18
- jobs:
19
- deploy:
20
- runs-on: ubuntu-latest
21
- if: ${{ github.event.workflow_run.conclusion == 'success' }}
22
- steps:
23
- - uses: actions/checkout@v3
24
- with:
25
- fetch-depth: 0
26
- - name: Cleanup README
27
- run: |
28
- sed -ri 's/^(##*)\s*:.*:\s*/\1 /g' README.md
29
- awk '{if (match($0,"## Supporters")) exit; print}' README.md > README
30
- mv -f README README.md
31
- - run: python3 -m pip install --upgrade build && python3 -m build
32
- - name: Upload ${{ env.package }} to PyPI
33
- uses: pypa/gh-action-pypi-publish@release/v1
34
- with:
35
- password: ${{ secrets.PYPI_API_TOKEN }}
36
- verbose: true
37
- verify_metadata: false
@@ -1 +0,0 @@
1
- 0.2.0
@@ -1,2 +0,0 @@
1
- lief>=0.16.1
2
- matplotlib
@@ -1,17 +0,0 @@
1
- #!/usr/bin/python
2
- # -*- coding: UTF-8 -*-
3
- import matplotlib.pyplot as plt
4
- import os
5
- from exeplot.plots.__common__ import _ensure_str, Binary
6
- from unittest import TestCase
7
-
8
-
9
- class TestOthers(TestCase):
10
- def test_miscellaneous(self):
11
- self.assertRaises(TypeError, _ensure_str, 1)
12
- for i in range(256):
13
- self.assertIsNotNone(_ensure_str(bytes([i])))
14
- self.assertRaises(TypeError, Binary, "BAD")
15
- binary = Binary(os.path.join(os.path.dirname(__file__), "hello.exe"))
16
- self.assertIsNotNone(str(binary))
17
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes