Skip to content

Commit 775c871

Browse files
committed
Applied various improvements
1 parent 65c2f58 commit 775c871

File tree

14 files changed

+141
-55
lines changed

14 files changed

+141
-55
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
22
<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>
33
<h3 align="center">Search for samples from various malware databases.</h3>
44

@@ -27,4 +27,4 @@ TODO
2727

2828
[![Forkers repo roster for @packing-box/python-exeplot](https://reporoster.com/forks/dark/packing-box/python-exeplot)](https://github.com/packing-box/python-exeplot/network/members)
2929

30-
<p align="center"><a href="#"><img src="https://img.shields.io/badge/Back%20to%20top--lightgrey?style=social" alt="Back to top" height="20"/></a></p>
30+
<p align="center"><a href="#top"><img src="https://img.shields.io/badge/Back%20to%20top--lightgrey?style=social" alt="Back to top" height="20"/></a></p>

pyproject.toml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ authors = [
2121
]
2222
description = "Library for plotting executable samples supporting multiple formats"
2323
license = {file = "LICENSE"}
24-
keywords = ["python", "development", "programming", "executable-samples", "plot"]
24+
keywords = ["python", "development", "programming", "executable-samples", "plot", "entropy", "cfg"]
2525
requires-python = ">=3.9,<4"
2626
classifiers = [
2727
"Development Status :: 5 - Production/Stable",
@@ -37,15 +37,23 @@ dependencies = [
3737
]
3838
dynamic = ["version"]
3939

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+
4048
[project.readme]
4149
file = "README.md"
4250
content-type = "text/markdown"
4351

4452
[project.urls]
4553
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"
4957

5058
[project.scripts]
5159
exeplot = "exeplot.__main__:main"

src/exeplot/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.1
1+
0.3.1

src/exeplot/__conf__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: UTF-8 -*-
22
import logging
33
import matplotlib.pyplot as plt
4+
import numpy
45
from functools import wraps
56

67

@@ -18,6 +19,8 @@
1819
'transparent': False,
1920
}
2021

22+
numpy.int = numpy.int_ # dirty fix to "AttributeError: module 'numpy' has no attribute 'int'."
23+
2124

2225
def configure(): # pragma: no cover
2326
from configparser import ConfigParser

src/exeplot/__info__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
44
"""
55
import os
6+
from datetime import datetime
7+
8+
__y = str(datetime.now().year)
9+
__s = "2025"
610

711
__author__ = "Alexandre D'Hondt"
8-
__copyright__ = 2025 A. D'Hondt"
12+
__copyright__ = {} A. D'Hondt".format([__y, __s + "-" + __y][__y != __s])
913
__email__ = "[email protected]"
1014
__license__ = "GPLv3 (https://www.gnu.org/licenses/gpl-3.0.fr.html)"
1115
__source__ = "https://github.com/packing-box/python-exeplot"
1216

1317
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
1418
__version__ = f.read().strip()
19+

src/exeplot/plots/__common__.py

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from functools import cached_property
44
from statistics import mean
55

6+
from ..utils import *
7+
68

79
CACHE_DIR = os.path.expanduser("~/.exeplot")
810
# https://matplotlib.org/2.0.2/examples/color/named_colors.html
@@ -48,36 +50,17 @@
4850
SHADOW = {'shade': .3, 'ox': .005, 'oy': -.005, 'linewidth': 0.}
4951
SUBLABELS = {
5052
'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),
5254
'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]),
5456
'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*']),
5658
'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,
5860
d['entropy*']),
5961
}
6062

6163

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-
8164
class Binary:
8265
def __init__(self, path, **kwargs):
8366
from lief import logging, parse
@@ -132,7 +115,7 @@ def __sections_data(self):
132115
h_len = b.header.header_size + b.header.program_header_size * b.header.numberof_segments
133116
elif self.type == "MachO":
134117
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"
136119
# then handle binary's sections
137120
color_cursor, i = 0, 1
138121
for section in sorted(b.sections, key=lambda s: s.offset):
@@ -145,30 +128,30 @@ def __sections_data(self):
145128
c = co[color_cursor % len(co)]
146129
color_cursor += 1
147130
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
149132
i += 1
150133
# sections header at the end for ELF files
151134
if self.type == "ELF":
152135
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"
154137
i += 1
155138
# finally, handle the overlay
156139
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"
158141
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"
160143

161144
def __segments_data(self):
162145
b = self.__binary
163146
if self.type == "PE":
164147
return # segments only apply to ELF and MachO
165148
elif self.type == "ELF":
166149
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)})", \
168151
s.file_offset, s.file_offset+s.physical_size, "lightgray"
169152
elif self.type == "MachO":
170153
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)})", \
172155
s.file_offset, s.file_offset+s.file_size, "lightgray"
173156

174157
def _data(self, segments=False, overlap=False):
@@ -255,7 +238,7 @@ def rawbytes(self):
255238

256239
@cached_property
257240
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}
259242
# names from string table only applies to PE
260243
if self.type != "PE":
261244
return names

src/exeplot/plots/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
__all__ = []
77

88

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__)))):
1010
if not f.endswith(".py") or f.startswith("_"):
1111
continue
1212
name = f[:-3]
1313
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")):
1515
globals()[f"{name}"] = f = getattr(module, "plot")
1616
f.__args__ = getattr(module, "arguments")
1717
f.__name__ = name

src/exeplot/plots/byte.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: UTF-8 -*-
2-
from .__common__ import _human_readable_size, Binary, COLORS
2+
from .__common__ import Binary, COLORS
33
from ..__conf__ import save_figure
4+
from ..utils import human_readable_size
45

56

67
def arguments(parser):

src/exeplot/plots/entropy.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# -*- coding: UTF-8 -*-
2-
from math import log2
3-
42
from .__common__ import mean, Binary, COLORS, MIN_ZONE_WIDTH, N_SAMPLES, SUBLABELS
53
from ..__conf__ import save_figure
4+
from ..utils import shannon_entropy
65

76

87
def arguments(parser):
@@ -26,12 +25,11 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
2625
:param n_samples: number of samples of entropy required
2726
:param window_size: window size for computing the entropy
2827
"""
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.
3028
binary = Binary(executable)
3129
data = {'hash': binary.hash, 'name': binary.basename, 'size': binary.size, 'type': binary.type,
3230
'entropy': [], 'sections': []}
3331
# compute window-based entropy
34-
data['entropy*'] = _entropy(binary.rawbytes)
32+
data['entropy*'] = shannon_entropy(binary.rawbytes)
3533
step, cs = abs(binary.size // n_samples), binary.size / n_samples # chunk size
3634
if isinstance(window_size, type(lambda: 0)):
3735
window_size = window_size(step)
@@ -47,7 +45,7 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
4745
window += f.read(new_pos - cur_pos if i > 0 else winter)
4846
window = window[max(0, len(window)-window_size) if cur_pos + winter < binary.size else step:]
4947
# compute entropy
50-
data['entropy'].append(_entropy(window)/8.)
48+
data['entropy'].append(shannon_entropy(window)/8.)
5149
# compute other characteristics using the Binary instance parsed with LIEF
5250
# convert to 3-tuple (EP offset on plot, EP file offset, section name containing EP)
5351
ep, ep_sec = binary.entrypoint, binary.entrypoint_section

src/exeplot/plots/nested_pie.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: UTF-8 -*-
2-
from .__common__ import _human_readable_size, Binary, COLORS, SHADOW
2+
from .__common__ import Binary, COLORS, SHADOW
33
from ..__conf__ import save_figure
4+
from ..utils import human_readable_size
45

56

67
def arguments(parser):

0 commit comments

Comments
 (0)