exeplot 0.4.2__tar.gz → 0.5.2__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.
- {exeplot-0.4.2 → exeplot-0.5.2}/.coveragerc +1 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/.github/workflows/python-package.yml +1 -1
- {exeplot-0.4.2 → exeplot-0.5.2}/PKG-INFO +18 -2
- {exeplot-0.4.2 → exeplot-0.5.2}/README.md +16 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/coverage.svg +1 -1
- exeplot-0.5.2/docs/pages/img/calc_packed_byte2.png +0 -0
- exeplot-0.5.2/docs/pages/img/calc_packed_entropy.png +0 -0
- exeplot-0.5.2/docs/pages/img/upx_calc_byte.png +0 -0
- exeplot-0.5.2/docs/pages/img/upx_calc_entropy.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/pyproject.toml +1 -1
- exeplot-0.5.2/src/exeplot/VERSION.txt +1 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__conf__.py +3 -3
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__main__.py +13 -4
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/byte.py +32 -30
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/diff.py +7 -6
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/entropy.py +32 -32
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/nested_pie.py +4 -3
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/pie.py +7 -4
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/utils.py +35 -14
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/PKG-INFO +18 -2
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/SOURCES.txt +4 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/tests/test_others.py +6 -3
- {exeplot-0.4.2 → exeplot-0.5.2}/tests/test_plots.py +15 -0
- exeplot-0.4.2/src/exeplot/VERSION.txt +0 -1
- {exeplot-0.4.2 → exeplot-0.5.2}/.gitignore +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/.readthedocs.yml +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/LICENSE +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/_config.yml +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/mkdocs.yml +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/css/extra.css +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_orig_entropy.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_packed_byte.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_packed_nested_pie.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_packed_pie.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/icon.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/logo.png +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/index.md +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/docs/requirements.txt +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/pytest.ini +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/requirements.txt +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/setup.cfg +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__info__.py +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__init__.py +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/__common__.py +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/__init__.py +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/graph.py +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/dependency_links.txt +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/entry_points.txt +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/requires.txt +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/top_level.txt +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/tests/__init__.py +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/tests/hello.elf +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/tests/hello.exe +0 -0
- {exeplot-0.4.2 → exeplot-0.5.2}/tests/hello.macho +0 -0
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
fail-fast: false
|
|
20
20
|
matrix:
|
|
21
21
|
os: [ubuntu-latest]
|
|
22
|
-
python-version: ["3.
|
|
22
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v3
|
|
25
25
|
- name: Set up Python ${{ matrix.python-version }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exeplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -689,7 +689,7 @@ Classifier: Intended Audience :: Developers
|
|
|
689
689
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
690
690
|
Classifier: Programming Language :: Python :: 3
|
|
691
691
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
692
|
-
Requires-Python: <4,>=3.
|
|
692
|
+
Requires-Python: <4,>=3.10
|
|
693
693
|
Description-Content-Type: text/markdown
|
|
694
694
|
License-File: LICENSE
|
|
695
695
|
Requires-Dist: lief>=0.16.1
|
|
@@ -730,6 +730,14 @@ $ exeplot byte calc_packed.exe
|
|
|
730
730
|
|
|
731
731
|

|
|
732
732
|
|
|
733
|
+
Draw a simplified byte plot of `calc_packed.exe`:
|
|
734
|
+
|
|
735
|
+
```sh
|
|
736
|
+
$ exeplot byte calc_packed.exe --no-title --no-legend
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+

|
|
740
|
+
|
|
733
741
|
Draw a pie plot of `calc_packed.exe`:
|
|
734
742
|
|
|
735
743
|
```sh
|
|
@@ -754,4 +762,12 @@ $ exeplot entropy calc_orig.exe calc_packed.exe
|
|
|
754
762
|
|
|
755
763
|

|
|
756
764
|
|
|
765
|
+
Draw a simplified entropy plot of `calc_packed.exe`:
|
|
766
|
+
|
|
767
|
+
```sh
|
|
768
|
+
$ exeplot entropy calc_packed.exe --no-title --no-legend --no-label --no-entrypoint
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+

|
|
772
|
+
|
|
757
773
|
|
|
@@ -26,6 +26,14 @@ $ exeplot byte calc_packed.exe
|
|
|
26
26
|
|
|
27
27
|

|
|
28
28
|
|
|
29
|
+
Draw a simplified byte plot of `calc_packed.exe`:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
$ exeplot byte calc_packed.exe --no-title --no-legend
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
29
37
|
Draw a pie plot of `calc_packed.exe`:
|
|
30
38
|
|
|
31
39
|
```sh
|
|
@@ -50,4 +58,12 @@ $ exeplot entropy calc_orig.exe calc_packed.exe
|
|
|
50
58
|
|
|
51
59
|

|
|
52
60
|
|
|
61
|
+
Draw a simplified entropy plot of `calc_packed.exe`:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
$ exeplot entropy calc_packed.exe --no-title --no-legend --no-label --no-entrypoint
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
53
69
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 96.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 96.54%"><title>coverage: 96.54%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#4c1"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">96.54%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">96.54%</text></g></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -22,7 +22,7 @@ authors = [
|
|
|
22
22
|
description = "Library for plotting executable samples supporting multiple formats"
|
|
23
23
|
license = {file = "LICENSE"}
|
|
24
24
|
keywords = ["python", "development", "programming", "executable-samples", "plot", "entropy", "cfg"]
|
|
25
|
-
requires-python = ">=3.
|
|
25
|
+
requires-python = ">=3.10,<4"
|
|
26
26
|
classifiers = [
|
|
27
27
|
"Development Status :: 5 - Production/Stable",
|
|
28
28
|
"Environment :: Console",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.2
|
|
@@ -29,7 +29,7 @@ config = {
|
|
|
29
29
|
numpy.int = numpy.int_ # dirty fix to "AttributeError: module 'numpy' has no attribute 'int'."
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def check_imports(*names):
|
|
32
|
+
def check_imports(*names) -> None:
|
|
33
33
|
import warnings
|
|
34
34
|
from inspect import currentframe
|
|
35
35
|
glob = currentframe().f_back.f_globals
|
|
@@ -42,7 +42,7 @@ def check_imports(*names):
|
|
|
42
42
|
glob['_IMP'] = False
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def configure(): # pragma: no cover
|
|
45
|
+
def configure() -> None: # pragma: no cover
|
|
46
46
|
from configparser import ConfigParser
|
|
47
47
|
from os.path import exists, expanduser
|
|
48
48
|
path = expanduser("~/.exeplot.conf")
|
|
@@ -58,7 +58,7 @@ def configure(): # pragma: no cover
|
|
|
58
58
|
plt.rcParams['font.family'] = config['font_family']
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def configure_fonts(**kw):
|
|
61
|
+
def configure_fonts(**kw) -> dict:
|
|
62
62
|
import matplotlib
|
|
63
63
|
matplotlib.rc('font', **{k.split("_")[1]: kw.pop(k, config[k]) for k in ['font_family', 'font_size']})
|
|
64
64
|
kw['title-font'] = {'fontfamily': kw.pop('title_font_family', config['font_family']),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
|
2
|
+
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
|
|
3
|
+
|
|
2
4
|
from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
|
|
3
5
|
from .__init__ import *
|
|
4
6
|
from .__init__ import __all__ as _plots
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
def _parser(name, description, examples):
|
|
8
|
-
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
9
|
+
def _parser(name: str, description: str, examples: list[str]) -> ArgumentParser:
|
|
9
10
|
descr = f"{name} {__version__}\n\nAuthor : {__author__} ({__email__})\nCopyright: {__copyright__}\nLicense :" \
|
|
10
11
|
f" {__license__}\nSource : {__source__}\n\n{description}.\n\n"
|
|
11
12
|
examples = [f"exeplot {e}" if not e.startswith("exeplot ") else e for e in examples]
|
|
@@ -13,7 +14,7 @@ def _parser(name, description, examples):
|
|
|
13
14
|
epilog="usage examples:\n " + "\n ".join(examples) if len(examples) > 0 else None)
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def _setup(parser): # pragma: no cover
|
|
17
|
+
def _setup(parser: ArgumentParser) -> Namespace: # pragma: no cover
|
|
17
18
|
args = parser.parse_args()
|
|
18
19
|
if hasattr(args, "verbose"):
|
|
19
20
|
import logging
|
|
@@ -22,7 +23,7 @@ def _setup(parser): # pragma: no cover
|
|
|
22
23
|
return args
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def main():
|
|
26
|
+
def main() -> None: # pragma: no cover
|
|
26
27
|
from os import makedirs
|
|
27
28
|
parser = _parser("Exeplot", "This tool allows to plot executable sample(s) in different ways",
|
|
28
29
|
["byte binary.exe", "entropy binary1.exe binary2.exe --scale"])
|
|
@@ -34,6 +35,14 @@ def main():
|
|
|
34
35
|
plot_func = globals()[plot]
|
|
35
36
|
plot_parser = plot_func.__args__(plots.add_parser(plot, help=plot_func.__doc__.strip(), add_help=False))
|
|
36
37
|
opt = plot_parser.add_argument_group("options")
|
|
38
|
+
if plot == "diff":
|
|
39
|
+
opt.add_argument("--no-colormap", action="store_true", help="do not display the color map (default: False)")
|
|
40
|
+
if plot == "entropy":
|
|
41
|
+
opt.add_argument("--no-entrypoint", action="store_true",
|
|
42
|
+
help="do not display the entry point (default: False)")
|
|
43
|
+
if plot in ["entropy", "pie"]:
|
|
44
|
+
opt.add_argument("--no-label", action="store_true", help="do not display the labels (default: False)")
|
|
45
|
+
opt.add_argument("--no-legend", action="store_true", help="do not display the legend (default: False)")
|
|
37
46
|
opt.add_argument("--no-title", action="store_true", help="do not display the title (default: False)")
|
|
38
47
|
extra = plot_parser.add_argument_group("extra arguments")
|
|
39
48
|
extra.add_argument("-h", "--help", action="help", help="show this help message and exit")
|
|
@@ -53,39 +53,41 @@ def plot(executable, height=600, grayscale=False, **kwargs):
|
|
|
53
53
|
pass
|
|
54
54
|
max_w = sum(max_txt_w) + (n_cols - 1) * txt_spacing
|
|
55
55
|
# draw a separator
|
|
56
|
-
|
|
56
|
+
if not kwargs.get('no_legend', False):
|
|
57
|
+
images.append(Image.new(m, (int(.05 * height), height), "white"))
|
|
57
58
|
# draw a sections plot aside
|
|
58
59
|
img = Image.new(m, (s, s), "white")
|
|
59
60
|
# draw the legend with section names
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
61
|
+
if not kwargs.get('no_legend', False):
|
|
62
|
+
legend = Image.new(m, (max_w, height), "white")
|
|
63
|
+
draw = ImageDraw.Draw(legend)
|
|
64
|
+
_xy = lambda n, c: (txt_spacing + sum(max_txt_w[:c]) + len(max_txt_w[:c]) * txt_spacing, \
|
|
65
|
+
txt_spacing + (n % n_lab_per_col) * (txt_spacing + txt_h))
|
|
66
|
+
_c_func = [_rgb, _gs][grayscale]
|
|
67
|
+
for i, name, start, end, color in binary:
|
|
68
|
+
if start != end:
|
|
69
|
+
x0, y0 = min(max(ceil(((start / factor) % s)) - 1, 0), s - 1), \
|
|
70
|
+
min(max(ceil(start / s / factor) - 1, 0), s - 1)
|
|
71
|
+
xN, yN = min(max(ceil(((end / factor) % s)) - 1, 0), s - 1), \
|
|
72
|
+
min(max(ceil(end / s / factor) - 1, 0), s - 1)
|
|
73
|
+
if y0 == yN:
|
|
74
|
+
xN = min(max(x0 + 1, xN), s - 1)
|
|
75
|
+
c = _c_func(color)
|
|
76
|
+
for x in range(x0, s if y0 < yN else xN):
|
|
77
|
+
img.putpixel((x, y0), c)
|
|
78
|
+
for y in range(y0 + 1, yN):
|
|
79
|
+
for x in range(0, s):
|
|
80
|
+
img.putpixel((x, y), c)
|
|
81
|
+
if yN > y0:
|
|
82
|
+
for x in range(0, xN):
|
|
83
|
+
img.putpixel((x, yN), c)
|
|
84
|
+
# fill the legend with the current section name
|
|
85
|
+
if name.startswith("TOTAL"):
|
|
86
|
+
color = "black"
|
|
87
|
+
draw.text(_xy(i, ceil((i + 1) / n_lab_per_col) - 1), name, fill=_c_func(color), font=font)
|
|
88
|
+
images.append(img.resize((int(img.size[0] * sf * .2), height), resample=Image.Resampling.BOX))
|
|
89
|
+
images.append(Image.new(m, (int(.03 * height), height), "white")) # draw another separator
|
|
90
|
+
images.append(legend)
|
|
89
91
|
# combine images horizontally
|
|
90
92
|
x, img = 0, Image.new(m, (sum(i.size[0] for i in images), height))
|
|
91
93
|
for i in images:
|
|
@@ -133,13 +133,14 @@ def plot(executable, executable2, legend1="", legend2="", **kwargs):
|
|
|
133
133
|
va="center")
|
|
134
134
|
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
|
|
135
135
|
logger.debug("> configuring the figure")
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
if not kwargs.get('no_colorbar', False):
|
|
137
|
+
cb = plt.colorbar(ScalarMappable(cmap=ListedColormap(colors, N=4)),
|
|
138
|
+
location='bottom', ax=objs[-1], fraction=0.3, aspect=50, ticks=[0.125, 0.375, 0.625, 0.875])
|
|
139
|
+
cb.set_ticklabels(['removed', 'modified', 'untouched', 'added'])
|
|
140
|
+
cb.ax.tick_params(length=0)
|
|
141
|
+
cb.outline.set_visible(False)
|
|
141
142
|
plt.subplots_adjust(left=[.15, .02][legend1 == "" and legend2 == ""], bottom=.5/max(1.75, nf))
|
|
142
143
|
h, l = (objs[int(title)] if nf+int(title) > 1 else objs).get_legend_handles_labels()
|
|
143
|
-
if len(h) > 0:
|
|
144
|
+
if len(h) > 0 and not kwargs.get('no_legend', False):
|
|
144
145
|
plt.figlegend(h, l, loc=[.8, .135], ncol=1, prop={'size': fs_ref*.7})
|
|
145
146
|
|
|
@@ -58,8 +58,7 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
|
|
|
58
58
|
name = binary.section_names[section.name]
|
|
59
59
|
start = max(data['sections'][-1][1] if len(data['sections']) > 0 else 0, int(section.offset // cs))
|
|
60
60
|
rawsize = max(section.size, len(section.content)) # take section header's raw size but consider real size too
|
|
61
|
-
max_end = min(max(start + MIN_ZONE_WIDTH, int((section.offset + rawsize) // cs)),
|
|
62
|
-
len(data['entropy']) - 1)
|
|
61
|
+
max_end = min(max(start + MIN_ZONE_WIDTH, int((section.offset + rawsize) // cs)), len(data['entropy']) - 1)
|
|
63
62
|
data['sections'].append(__d(int(min(start, max_end - MIN_ZONE_WIDTH)), int(max_end), name))
|
|
64
63
|
# adjust the entry point (be sure that its position on the plot is within the EP section)
|
|
65
64
|
if data['ep']:
|
|
@@ -121,36 +120,37 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
|
|
|
121
120
|
fig.suptitle(f"Entropy per section of {d['type']} file: {target or d['name']}", x=x_t, y=y_t,
|
|
122
121
|
ha="center", va="bottom", **kwargs['title-font'])
|
|
123
122
|
# set the label and sublabel and display them
|
|
124
|
-
try:
|
|
125
|
-
label = labels[i]
|
|
126
|
-
if isinstance(label, type(lambda: 0)):
|
|
127
|
-
label = label(d)
|
|
128
|
-
except:
|
|
129
|
-
pass
|
|
130
123
|
ref_point = .55
|
|
131
|
-
if
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
124
|
+
if not kwargs.get('no_label', False):
|
|
125
|
+
try:
|
|
126
|
+
label = labels[i]
|
|
127
|
+
if isinstance(label, type(lambda: 0)):
|
|
128
|
+
label = label(d)
|
|
129
|
+
except:
|
|
130
|
+
pass
|
|
131
|
+
if sublabel and not (isinstance(sublabel, str) and "ep" in sublabel and d['ep'] is None):
|
|
132
|
+
if isinstance(sublabel, str):
|
|
133
|
+
sublabel = SUBLABELS.get(sublabel)
|
|
134
|
+
sl = sublabel(d) if isinstance(sublabel, type(lambda: 0)) else None
|
|
135
|
+
if sl:
|
|
136
|
+
nl, y_pos, f_color = len(sl.split("\n")), ref_point, "black"
|
|
137
|
+
if label:
|
|
138
|
+
f_size, f_color = fs_ref*.6 if nl <= 2 else fs_ref*.5, "gray"
|
|
139
|
+
y_pos = max(0., ref_point - nl * [.16, .12, .09, .08][min(4, nl)-1])
|
|
140
|
+
else:
|
|
141
|
+
f_size = fs_ref * [.7, .6, .5][min(3, nl)-1]
|
|
142
|
+
obj.text(s=sl, x=-420., y=y_pos, fontsize=f_size, color=f_color, ha="left", va="center")
|
|
143
|
+
if label:
|
|
144
|
+
y_pos = ref_point
|
|
145
|
+
if sublabel:
|
|
146
|
+
nl = len(sl.split("\n"))
|
|
147
|
+
y_pos = min(1.3, ref_point + .3 + nl * [.16, .12, .09, .08][min(4, nl)-1])
|
|
148
|
+
obj.text(s=label, x=-420., y=y_pos, fontsize=fs_ref, ha="left", va="center")
|
|
149
|
+
h, h_midlen = d['hash'], round(len(d['hash'])/2+.5)
|
|
150
|
+
h = f"{h[:h_midlen]}\n{h[h_midlen:]}"
|
|
151
|
+
obj.text(s=h, x=-420., y=y_pos-.35, fontsize=fs_ref*.5, ha="left", va="center")
|
|
152
152
|
# display the entry point
|
|
153
|
-
if d['ep']:
|
|
153
|
+
if d['ep'] and not kwargs.get('no_entrypoint', False):
|
|
154
154
|
obj.vlines(x=d['ep'][0], ymin=0, ymax=1, color="r", zorder=11).set_label("Entry point")
|
|
155
155
|
obj.text(d['ep'][0], -.15, "______", c="r", ha="center", rotation=90, size=.8,
|
|
156
156
|
bbox={'boxstyle': "rarrow", 'fc': "r", 'ec': "r", 'lw': 1})
|
|
@@ -166,7 +166,7 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
|
|
|
166
166
|
color_cursor += 1
|
|
167
167
|
# draw the section
|
|
168
168
|
obj.fill_between(x, 0, 1, facecolor=c, alpha=.2)
|
|
169
|
-
if name not in ["Headers", "Overlay"]:
|
|
169
|
+
if name not in ["Headers", "Overlay"] and not kwargs.get('no_label', False):
|
|
170
170
|
if last is None or (start + end) // 2 - (last[0] + last[1]) // 2 > n // 12:
|
|
171
171
|
pos_y = N_TOP
|
|
172
172
|
else:
|
|
@@ -197,6 +197,6 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
|
|
|
197
197
|
h, l = (objs[[0, 1][title]] if nf+[0, 1][title] > 1 else objs).get_legend_handles_labels()
|
|
198
198
|
h.append(Patch(facecolor="black")), l.append("Headers")
|
|
199
199
|
h.append(Patch(facecolor="lightgray")), l.append("Overlay")
|
|
200
|
-
if len(h) > 0:
|
|
200
|
+
if len(h) > 0 and not kwargs.get('no_legend', False):
|
|
201
201
|
plt.figlegend(h, l, loc=lloc, ncol=1 if lloc_side else len(l), prop={'size': fs_ref*.7})
|
|
202
202
|
|
|
@@ -32,9 +32,10 @@ def plot(executable, **kwargs):
|
|
|
32
32
|
ax.pie(w, radius=1-i*size, colors=colors, startangle=90, wedgeprops={'width': size}, **pie_kw)
|
|
33
33
|
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
|
|
34
34
|
logger.debug("> configuring the figure")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
if not kwargs.get('no_legend', False):
|
|
36
|
+
ncols = ceil(len(legend['colors']) / 12)
|
|
37
|
+
ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
|
|
38
|
+
bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(kwargs['config']['font_size']*.7))
|
|
38
39
|
if not kwargs.get('no_title', False):
|
|
39
40
|
fsp = plt.gcf().subplotpars
|
|
40
41
|
plt.title(f"Nested pie plot of {binary.type} file: {binary.basename}", **kwargs['title-font'])
|
|
@@ -23,16 +23,19 @@ def plot(executable, donut=False, **kwargs):
|
|
|
23
23
|
for _, _, data, _, colors, legend in binary._data():
|
|
24
24
|
pass
|
|
25
25
|
ncols, n = ceil(len(legend['colors']) / 12), sum(data)
|
|
26
|
-
labels = [f"{100 * d / n:.1f}%" if round(d / n, 2) >= .02 and c != "white" else "" for d, c in zip(data, colors)]
|
|
27
26
|
txt_kw = {k: v for k, v in kwargs['xlabel-font'].items()}
|
|
28
27
|
txt_kw['color'] = "w"
|
|
29
28
|
pie_kw = {'shadow': SHADOW} if kwargs['config']['shadow'] else {}
|
|
30
|
-
|
|
29
|
+
if not kwargs.get('no_label', False):
|
|
30
|
+
pie_kw['labels'] = [f"{100 * d / n:.1f}%" if round(d / n, 2) >= .02 and c != "white" else "" \
|
|
31
|
+
for d, c in zip(data, colors)]
|
|
32
|
+
_, texts = ax.pie(data, colors=colors, textprops=txt_kw, labeldistance=.55, startangle=90,
|
|
31
33
|
wedgeprops={'width': [1., .55][donut]}, **pie_kw)
|
|
32
34
|
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
|
|
33
35
|
logger.debug("> configuring the figure")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if not kwargs.get('no_legend', False):
|
|
37
|
+
ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
|
|
38
|
+
bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(fs_ref*.7))
|
|
36
39
|
plt.setp(texts, size=fs_ref*.8, weight="bold")
|
|
37
40
|
if not kwargs.get('no_title', False):
|
|
38
41
|
fsp = plt.gcf().subplotpars
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
|
2
|
+
import numpy as np
|
|
2
3
|
from math import log2
|
|
4
|
+
from typing import Optional
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
__all__ = ["ensure_str", "human_readable_size", "ngrams_counts", "ngrams_distribution", "shannon_entropy"]
|
|
@@ -7,7 +9,7 @@ __all__ = ["ensure_str", "human_readable_size", "ngrams_counts", "ngrams_distrib
|
|
|
7
9
|
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
10
|
|
|
9
11
|
|
|
10
|
-
def ensure_str(s, encoding=
|
|
12
|
+
def ensure_str(s: str | bytes, encoding: str = "utf-8", errors: str = "strict") -> str:
|
|
11
13
|
""" Ensure that an input string is decoded. """
|
|
12
14
|
if isinstance(s, bytes):
|
|
13
15
|
try:
|
|
@@ -19,7 +21,7 @@ def ensure_str(s, encoding='utf-8', errors='strict'):
|
|
|
19
21
|
return s
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def human_readable_size(size, precision=0):
|
|
24
|
+
def human_readable_size(size: int, precision: int = 0) -> str:
|
|
23
25
|
""" Display bytes' size in a human-readable format given a precision. """
|
|
24
26
|
i, units = 0, ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
25
27
|
while size >= 1024 and i < len(units)-1:
|
|
@@ -28,27 +30,45 @@ def human_readable_size(size, precision=0):
|
|
|
28
30
|
return "%.*f%s" % (precision, size, units[i])
|
|
29
31
|
|
|
30
32
|
|
|
31
|
-
def ngrams_counts(byte_obj, n=1, step=1):
|
|
33
|
+
def ngrams_counts(byte_obj: bytes | object, n: int = 1, step: int = 1) -> dict[bytes, int]:
|
|
32
34
|
""" Output the Counter instance for an input byte sequence or byte object based on n-grams.
|
|
33
35
|
If the input is a byte object, cache the result.
|
|
34
36
|
|
|
35
37
|
:param byte_obj: byte sequence ('bytes') or byte object with "bytes" and "size" attributes (i.e. pathlib2.Path)
|
|
36
38
|
:param n: n determining the size of n-grams, defaults to 1
|
|
37
39
|
:param step: step for sliding the n-grams
|
|
40
|
+
:param start: number of bytes to start from
|
|
38
41
|
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
if n not in (1, 2, 3):
|
|
43
|
+
raise ValueError("n must be 1, 2, or 3")
|
|
44
|
+
if step <= 0:
|
|
45
|
+
raise ValueError("step must be positive")
|
|
46
|
+
if isinstance(byte_obj, bytes) or hasattr(byte_obj, "bytes"):
|
|
47
|
+
a = np.frombuffer(data := byte_obj if isinstance(byte_obj, bytes) else byte_obj.bytes, dtype=np.uint8)
|
|
48
|
+
l = a.size
|
|
49
|
+
if l < n:
|
|
50
|
+
return {}
|
|
51
|
+
if n == 1:
|
|
52
|
+
counts = {b.to_bytes(1, "big"): int(c) for b, c in \
|
|
53
|
+
enumerate(np.bincount(np.frombuffer(data, dtype=np.uint8)))}
|
|
54
|
+
else:
|
|
55
|
+
end = (m := (l - n) // step + 1) * step
|
|
56
|
+
grams = np.stack((a[0:end:step], a[1:1+end:step]), axis=1) if n == 2 else \
|
|
57
|
+
np.stack((a[0:end:step], a[1:1+end:step], a[2:2+end:step]), axis=1)
|
|
58
|
+
counts = {bytes(row): int(c) for row, c in zip(*np.unique(grams, axis=0, return_counts=True))}
|
|
59
|
+
if isinstance(byte_obj, bytes):
|
|
60
|
+
return counts
|
|
61
|
+
elif hasattr(byte_obj, "bytes"):
|
|
62
|
+
if not hasattr(byte_obj, "_ngram_counts_cache"):
|
|
63
|
+
byte_obj._ngram_counts_cache = {}
|
|
64
|
+
if n not in byte_obj._ngram_counts_cache.keys():
|
|
65
|
+
byte_obj._ngram_counts_cache[n] = counts
|
|
66
|
+
return byte_obj._ngram_counts_cache[n]
|
|
48
67
|
raise TypeError("Bad input type ; should be a byte sequence or object")
|
|
49
68
|
|
|
50
69
|
|
|
51
|
-
def ngrams_distribution(byte_obj, n=1, step=1, n_most_common
|
|
70
|
+
def ngrams_distribution(byte_obj: bytes | object, n: int = 1, step: int = 1, n_most_common: Optional[int] = None,
|
|
71
|
+
n_exclude_top: int = 0, exclude: Optional[list] = None) -> list[tuple[bytes, int]]:
|
|
52
72
|
""" Compute the n-grams distribution of an input byte sequence or byte object given exclusions.
|
|
53
73
|
|
|
54
74
|
:param byte_obj: byte sequence ('bytes') or byte object with "bytes" and "size" attributes (i.e. pathlib2.Path)
|
|
@@ -60,7 +80,8 @@ def ngrams_distribution(byte_obj, n=1, step=1, n_most_common=None, n_exclude_top
|
|
|
60
80
|
:return: list of n_most_common (n-gram, count) pairs
|
|
61
81
|
"""
|
|
62
82
|
c = ngrams_counts(byte_obj, n, step)
|
|
63
|
-
|
|
83
|
+
n = len(c) if n_most_common is None else n_most_common + n_exclude_top + len(exclude or [])
|
|
84
|
+
r = sorted(c.items(), key=lambda p: p[1], reverse=True)[:n]
|
|
64
85
|
if exclude is not None:
|
|
65
86
|
r = [(ngram, count) for ngram, count in r if ngram not in exclude]
|
|
66
87
|
return r[n_exclude_top:n_exclude_top+(n_most_common or len(c))]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exeplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -689,7 +689,7 @@ Classifier: Intended Audience :: Developers
|
|
|
689
689
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
690
690
|
Classifier: Programming Language :: Python :: 3
|
|
691
691
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
692
|
-
Requires-Python: <4,>=3.
|
|
692
|
+
Requires-Python: <4,>=3.10
|
|
693
693
|
Description-Content-Type: text/markdown
|
|
694
694
|
License-File: LICENSE
|
|
695
695
|
Requires-Dist: lief>=0.16.1
|
|
@@ -730,6 +730,14 @@ $ exeplot byte calc_packed.exe
|
|
|
730
730
|
|
|
731
731
|

|
|
732
732
|
|
|
733
|
+
Draw a simplified byte plot of `calc_packed.exe`:
|
|
734
|
+
|
|
735
|
+
```sh
|
|
736
|
+
$ exeplot byte calc_packed.exe --no-title --no-legend
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+

|
|
740
|
+
|
|
733
741
|
Draw a pie plot of `calc_packed.exe`:
|
|
734
742
|
|
|
735
743
|
```sh
|
|
@@ -754,4 +762,12 @@ $ exeplot entropy calc_orig.exe calc_packed.exe
|
|
|
754
762
|
|
|
755
763
|

|
|
756
764
|
|
|
765
|
+
Draw a simplified entropy plot of `calc_packed.exe`:
|
|
766
|
+
|
|
767
|
+
```sh
|
|
768
|
+
$ exeplot entropy calc_packed.exe --no-title --no-legend --no-label --no-entrypoint
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+

|
|
772
|
+
|
|
757
773
|
|
|
@@ -15,10 +15,14 @@ docs/pages/index.md
|
|
|
15
15
|
docs/pages/css/extra.css
|
|
16
16
|
docs/pages/img/calc_orig_entropy.png
|
|
17
17
|
docs/pages/img/calc_packed_byte.png
|
|
18
|
+
docs/pages/img/calc_packed_byte2.png
|
|
19
|
+
docs/pages/img/calc_packed_entropy.png
|
|
18
20
|
docs/pages/img/calc_packed_nested_pie.png
|
|
19
21
|
docs/pages/img/calc_packed_pie.png
|
|
20
22
|
docs/pages/img/icon.png
|
|
21
23
|
docs/pages/img/logo.png
|
|
24
|
+
docs/pages/img/upx_calc_byte.png
|
|
25
|
+
docs/pages/img/upx_calc_entropy.png
|
|
22
26
|
src/exeplot/VERSION.txt
|
|
23
27
|
src/exeplot/__conf__.py
|
|
24
28
|
src/exeplot/__info__.py
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# -*- coding: UTF-8 -*-
|
|
3
3
|
import matplotlib.pyplot as plt
|
|
4
4
|
import os
|
|
5
|
-
from collections import Counter
|
|
6
5
|
from exeplot.plots.__common__ import Binary
|
|
7
6
|
from exeplot.utils import *
|
|
8
7
|
from unittest import TestCase
|
|
@@ -21,10 +20,14 @@ class TestOthers(TestCase):
|
|
|
21
20
|
class TestUtils(TestCase):
|
|
22
21
|
def test_ngrams_functions(self):
|
|
23
22
|
self.assertRaises(TypeError, ngrams_counts, 123)
|
|
24
|
-
|
|
23
|
+
for n in [0, 4]:
|
|
24
|
+
self.assertRaises(ValueError, ngrams_counts, b"abc", n=n)
|
|
25
|
+
self.assertRaises(ValueError, ngrams_counts, b"abc", step=-1)
|
|
26
|
+
self.assertEqual(ngrams_counts(b"a", n=2), {})
|
|
27
|
+
self.assertTrue(isinstance(ngrams_counts(seq := b"\x00" * 4 + os.urandom(120) + b"\xff" * 4), dict))
|
|
28
|
+
self.assertTrue(isinstance(ngrams_counts(seq := b"\x00" * 4 + os.urandom(120) + b"\xff" * 4, n=2), dict))
|
|
25
29
|
class Test:
|
|
26
30
|
bytes = seq
|
|
27
|
-
size = len(seq)
|
|
28
31
|
histogram = ngrams_distribution(t := Test(), exclude=(b"\x00", b"\xff"))
|
|
29
32
|
self.assertTrue(isinstance(histogram, list))
|
|
30
33
|
self.assertNotIn(b"\x00", [b for b, c in histogram])
|
|
@@ -6,6 +6,7 @@ from exeplot import *
|
|
|
6
6
|
from exeplot import __all__ as _plots
|
|
7
7
|
from exeplot.__conf__ import configure
|
|
8
8
|
from exeplot.__main__ import _parser
|
|
9
|
+
from itertools import cycle
|
|
9
10
|
from unittest import TestCase
|
|
10
11
|
|
|
11
12
|
|
|
@@ -69,6 +70,13 @@ class TestPlotOptions(TestCase):
|
|
|
69
70
|
scale=True):
|
|
70
71
|
os.remove(img)
|
|
71
72
|
plt.clf()
|
|
73
|
+
g = cycle(iter_files())
|
|
74
|
+
for k in ["title", "legend", "label", "entrypoint"]:
|
|
75
|
+
path, kw = next(g), {(k := f'no_{k}'): True}
|
|
76
|
+
print(f"plotting entropy of {path} ({k}=True)...")
|
|
77
|
+
for img in entropy(path, **kw):
|
|
78
|
+
os.remove(img)
|
|
79
|
+
plt.clf()
|
|
72
80
|
|
|
73
81
|
def test_pie_plot_function(self):
|
|
74
82
|
for path in iter_files():
|
|
@@ -76,4 +84,11 @@ class TestPlotOptions(TestCase):
|
|
|
76
84
|
for img in pie(path, donut=True):
|
|
77
85
|
os.remove(img)
|
|
78
86
|
plt.clf()
|
|
87
|
+
g = cycle(iter_files())
|
|
88
|
+
for k in ["title", "legend", "label"]:
|
|
89
|
+
path, kw = next(g), {(k := f'no_{k}'): True}
|
|
90
|
+
print(f"plotting pie of {path} ({k}=True)...")
|
|
91
|
+
for img in pie(path, **kw):
|
|
92
|
+
os.remove(img)
|
|
93
|
+
plt.clf()
|
|
79
94
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.4.2
|
|
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
|
|
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
|