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.
- {exeplot-0.2.0 → exeplot-0.3.1}/.github/workflows/python-package.yml +33 -3
- {exeplot-0.2.0 → exeplot-0.3.1}/PKG-INFO +13 -7
- {exeplot-0.2.0 → exeplot-0.3.1}/README.md +1 -1
- {exeplot-0.2.0 → exeplot-0.3.1}/pyproject.toml +12 -4
- exeplot-0.3.1/src/exeplot/VERSION.txt +1 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__conf__.py +3 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__info__.py +6 -1
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/__common__.py +14 -31
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/__init__.py +3 -2
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/byte.py +2 -1
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/entropy.py +3 -5
- exeplot-0.3.1/src/exeplot/plots/graph.py +44 -0
- exeplot-0.2.0/src/exeplot/plots/nested-pie.py → exeplot-0.3.1/src/exeplot/plots/nested_pie.py +2 -1
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/pie.py +2 -1
- exeplot-0.3.1/src/exeplot/utils.py +63 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/PKG-INFO +13 -7
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/SOURCES.txt +3 -2
- exeplot-0.3.1/src/exeplot.egg-info/requires.txt +8 -0
- exeplot-0.3.1/tests/test_others.py +34 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/tests/test_plots.py +9 -3
- exeplot-0.2.0/.github/workflows/pypi-publish.yml +0 -37
- exeplot-0.2.0/src/exeplot/VERSION.txt +0 -1
- exeplot-0.2.0/src/exeplot.egg-info/requires.txt +0 -2
- exeplot-0.2.0/tests/test_others.py +0 -17
- {exeplot-0.2.0 → exeplot-0.3.1}/.coveragerc +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/.gitignore +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/.readthedocs.yml +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/LICENSE +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/_config.yml +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/coverage.svg +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/mkdocs.yml +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/css/extra.css +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/img/icon.png +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/img/logo.png +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/pages/index.md +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/docs/requirements.txt +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/pytest.ini +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/requirements.txt +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/setup.cfg +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__init__.py +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/__main__.py +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot/plots/diff.py +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/dependency_links.txt +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/entry_points.txt +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/src/exeplot.egg-info/top_level.txt +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/tests/__init__.py +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/tests/hello.elf +0 -0
- {exeplot-0.2.0 → exeplot-0.3.1}/tests/hello.exe +0 -0
- {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
|
|
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
|
|
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@
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: exeplot
|
|
3
|
-
Version: 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/
|
|
683
|
-
Project-URL: issues, https://github.com/
|
|
684
|
-
Project-URL: repository, https://github.com/
|
|
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/
|
|
47
|
-
issues = "https://github.com/
|
|
48
|
-
repository = "https://github.com/
|
|
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__ = "©
|
|
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" %
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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 ({
|
|
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]} ({
|
|
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 ({
|
|
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 ({
|
|
140
|
+
yield i, f"[{i}] Overlay ({human_readable_size(end - start)})", start, self.size, "lightgray"
|
|
158
141
|
i += 1
|
|
159
|
-
yield i, f"TOTAL: {
|
|
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]} ({
|
|
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} ({
|
|
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:
|
|
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,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*'] =
|
|
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(
|
|
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
|
+
|
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: exeplot
|
|
3
|
-
Version: 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/
|
|
683
|
-
Project-URL: issues, https://github.com/
|
|
684
|
-
Project-URL: repository, https://github.com/
|
|
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/
|
|
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,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',
|
|
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",
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|