Skip to content

Commit 44b82e9

Browse files
committed
Applied some minor improvements
1 parent a15726d commit 44b82e9

File tree

9 files changed

+80
-46
lines changed

9 files changed

+80
-46
lines changed

src/exeplot/__conf__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
# -*- coding: UTF-8 -*-
22
import logging
3-
import matplotlib.pyplot as plt
43
import numpy
54
from functools import wraps
5+
from warnings import filterwarnings
6+
7+
filterwarnings("ignore", "Unable to import Axes3D.")
8+
9+
import matplotlib.pyplot as plt
10+
11+
12+
__all__ = ["check_imports", "config", "logger", "save_figure"]
613

714

815
logger = logging.getLogger("exeplot")
916
config = {
1017
'bbox_inches': "tight",
11-
# 'colormap_main': "RdYlGn_r",
12-
# 'colormap_other': "jet",
18+
'colormap_main': "RdYlGn_r",
19+
'colormap_other': "jet",
1320
'dpi': 300,
1421
'font_family': "serif",
1522
'font_size': 10,
@@ -99,6 +106,7 @@ def _wrapper(*a, **kw):
99106
ns.update(locals())
100107
interact(local=ns)
101108
logger.info(f"Saving to {img}...")
109+
plt.set_cmap("gray" if kw.get('grayscale', False) else config['colormap_main'])
102110
plt.savefig(img, **kw_plot)
103111
logger.debug(f"> saved to {img}...")
104112
r.append(img)

src/exeplot/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def _setup(parser): # pragma: no cover
1818
if hasattr(args, "verbose"):
1919
import logging
2020
logging.basicConfig(level=[logging.INFO, logging.DEBUG][args.verbose])
21+
logging.getLogger("exeplot").level = [logging.INFO, logging.DEBUG][args.verbose]
2122
return args
2223

2324

src/exeplot/plots/__common__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
'cstring': "navy", # string table
4242
'const': "cornflowerblue", # read-only data
4343
'literal4': "blue", # 4-byte literal values
44-
'literal4': "mediumblue", # 8-byte literal values
44+
'literal8': "mediumblue", # 8-byte literal values
4545
'common': "royalblue", # uninitialized imported symbol definitions
4646
}
4747
MIN_ZONE_WIDTH = 3 # minimum number of samples on the entropy plot for a section (so that it can still be visible even

src/exeplot/plots/byte.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
# -*- coding: UTF-8 -*-
22
from .__common__ import Binary, COLORS
3-
from ..__conf__ import save_figure
3+
from ..__conf__ import *
44
from ..utils import human_readable_size
55

66

77
def arguments(parser):
88
parser.add_argument("executable", help="executable sample to be plotted")
9+
parser.add_argument("-g", "--grayscale", action="store_true", help="use grayscale colors instead of RGB")
910
return parser
1011

1112

1213
@save_figure
13-
def plot(executable, height=600, **kwargs):
14+
def plot(executable, height=600, grayscale=False, **kwargs):
1415
""" draw a byte plot of the input binary """
1516
import matplotlib.colors as mcol
1617
import matplotlib.pyplot as plt
1718
from math import ceil, sqrt
1819
from matplotlib import font_manager, rcParams
1920
from PIL import Image, ImageDraw, ImageFont
21+
# ------------------------------------------------- DRAW THE PLOT --------------------------------------------------
2022
# determine base variables and some helper functions
21-
images, binary = [], Binary(executable)
22-
n_pixels = ceil(binary.size / 3)
23+
images, binary, factor = [], Binary(executable), [3, 1][grayscale]
24+
n_pixels = ceil(binary.size / factor)
2325
s = int(ceil(sqrt(n_pixels)))
2426
sf = height / s
2527
_rgb = lambda c: tuple(map(lambda x: int(255 * x), mcol.to_rgba(c)))
28+
_gs = lambda c: int(sum(a * b for a, b in zip([.299, .587, .114], _rgb(c))))
2629
# draw a byte plot
27-
rawbytes = binary.rawbytes + int((s * s * 3) - len(binary.rawbytes)) * b'\xff'
28-
images.append(Image.frombuffer("RGB", (s, s), rawbytes, "raw", "RGB", 0, 1) \
30+
logger.debug("> creating the main plot from raw bytes")
31+
rawbytes = binary.rawbytes + int((s * s * factor) - len(binary.rawbytes)) * b'\xff'
32+
images.append(Image.frombuffer(m := ["RGB", "L"][grayscale], (s, s), rawbytes, "raw", m, 0, 1) \
2933
.resize((int(s * sf), height), resample=Image.Resampling.BOX))
34+
logger.debug("> creating the side plot with sections")
3035
if len(binary.sections) > 0:
3136
# map matplotlib font to PIL ImageFont path
3237
font_name = rcParams[f"font.{kwargs['config']['font_family']}"][0]
@@ -35,7 +40,6 @@ def plot(executable, height=600, **kwargs):
3540
txt_spacing = txt_h // 2
3641
n_lab_per_col = int((height - txt_spacing) / (txt_h + txt_spacing))
3742
n_cols = ceil((len(binary.sections) + 2) / n_lab_per_col)
38-
#n_cols = 1
3943
max_txt_w = [0] * n_cols
4044
sections = ["Headers"] + [s for s in binary.sections] + ["Overlay"]
4145
draw = ImageDraw.Draw(images[0])
@@ -49,44 +53,48 @@ def plot(executable, height=600, **kwargs):
4953
pass
5054
max_w = sum(max_txt_w) + (n_cols - 1) * txt_spacing
5155
# draw a separator
52-
images.append(Image.new("RGB", (int(.05 * height), height), "white"))
56+
images.append(Image.new(m, (int(.05 * height), height), "white"))
5357
# draw a sections plot aside
54-
img = Image.new("RGB", (s, s), "white")
58+
img = Image.new(m, (s, s), "white")
5559
# draw the legend with section names
56-
legend = Image.new("RGB", (max_w, height), "white")
60+
legend = Image.new(m, (max_w, height), "white")
5761
draw = ImageDraw.Draw(legend)
5862
_xy = lambda n, c: (txt_spacing + sum(max_txt_w[:c]) + len(max_txt_w[:c]) * txt_spacing, \
5963
txt_spacing + (n % n_lab_per_col) * (txt_spacing + txt_h))
64+
_c_func = [_rgb, _gs][grayscale]
6065
for i, name, start, end, color in binary:
6166
if start != end:
62-
x0, y0 = min(max(ceil(((start / 3) % s)) - 1, 0), s - 1), \
63-
min(max(ceil(start / s / 3) - 1, 0), s - 1)
64-
xN, yN = min(max(ceil(((end / 3) % s)) - 1, 0), s - 1), \
65-
min(max(ceil(end / s / 3) - 1, 0), s - 1)
67+
x0, y0 = min(max(ceil(((start / factor) % s)) - 1, 0), s - 1), \
68+
min(max(ceil(start / s / factor) - 1, 0), s - 1)
69+
xN, yN = min(max(ceil(((end / factor) % s)) - 1, 0), s - 1), \
70+
min(max(ceil(end / s / factor) - 1, 0), s - 1)
6671
if y0 == yN:
6772
xN = min(max(x0 + 1, xN), s - 1)
73+
c = _c_func(color)
6874
for x in range(x0, s if y0 < yN else xN):
69-
img.putpixel((x, y0), _rgb(color))
75+
img.putpixel((x, y0), c)
7076
for y in range(y0 + 1, yN):
7177
for x in range(0, s):
72-
img.putpixel((x, y), _rgb(color))
78+
img.putpixel((x, y), c)
7379
if yN > y0:
7480
for x in range(0, xN):
75-
img.putpixel((x, yN), _rgb(color))
81+
img.putpixel((x, yN), c)
7682
# fill the legend with the current section name
7783
if name.startswith("TOTAL"):
7884
color = "black"
79-
draw.text(_xy(i, ceil((i + 1) / n_lab_per_col) - 1), name, fill=_rgb(color), font=font)
85+
draw.text(_xy(i, ceil((i + 1) / n_lab_per_col) - 1), name, fill=_c_func(color), font=font)
8086
images.append(img.resize((int(img.size[0] * sf * .2), height), resample=Image.Resampling.BOX))
81-
images.append(Image.new("RGB", (int(.03 * height), height), "white")) # draw another separator
87+
images.append(Image.new(m, (int(.03 * height), height), "white")) # draw another separator
8288
images.append(legend)
8389
# combine images horizontally
84-
x, img = 0, Image.new("RGB", (sum(i.size[0] for i in images), height))
90+
x, img = 0, Image.new(m, (sum(i.size[0] for i in images), height))
8591
for i in images:
8692
img.paste(i, (x, 0))
8793
x += i.size[0]
94+
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
8895
# plot combined PIL images
8996
#plt.tight_layout(pad=0)
97+
logger.debug("> configuring the figure")
9098
if not kwargs.get('no_title', False):
9199
# set plot's title before displaying the image
92100
fsp = plt.gcf().subplotpars

src/exeplot/plots/diff.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
from .__common__ import Binary, CACHE_DIR, COLORS, MIN_ZONE_WIDTH
3-
from ..__conf__ import save_figure
3+
from ..__conf__ import *
44

55

66
def arguments(parser):
@@ -67,10 +67,15 @@ def plot(executable, executable2, legend1="", legend2="", **kwargs):
6767
cache = Memory(CACHE_DIR, verbose=0).cache
6868
except ImportError:
6969
cache = lambda x: x # do nothing if joblib not installed
70+
bin1, bin2 = Binary(executable), Binary(executable2)
71+
if bin1.type != bin2.type:
72+
raise ValueError(f"Inputs executables have different types ({bin1.type} != {bin2.type})")
7073
# inner function for caching sequence matches)
7174
@cache
7275
def byte_differences(bytes1, bytes2):
7376
return zip(*SequenceMatcher(a=bytes1, b=bytes2).get_opcodes())
77+
# ------------------------------------------------- DRAW THE PLOT --------------------------------------------------
78+
logger.debug("> computing the difference between executables' raw bytes")
7479
fs_ref = kwargs['config']['font_size']
7580
title = not kwargs.get('no_title', False)
7681
lloc = kwargs.get('legend_location', "lower right")
@@ -81,16 +86,13 @@ def byte_differences(bytes1, bytes2):
8186
fig.tight_layout(pad=2)
8287
objs[-1].axis("off")
8388
values, colors = {'delete': 0, 'replace': 1, 'equal': 2, 'insert': 3}, ["red", "gold", "lightgray", "green"]
84-
bin1, bin2 = Binary(executable), Binary(executable2)
85-
if bin1.type != bin2.type:
86-
raise ValueError(f"Inputs executables have different types ({bin1.type} != {bin2.type})")
8789
if title:
8890
fig.suptitle(f"Byte-wise difference of {bin1.type} files: {bin1.basename} VS {bin2.basename}",
8991
x=[.5, .55][legend1 is None], y=1, ha="center", va="bottom", **kwargs['title-font'])
9092
legend1, legend2 = legend1 or bin1.basename, legend2 or bin2.basename
91-
(lg := kwargs['logger']).info("Matching binaries' byte sequences, this may take a while...")
93+
logger.info("Matching binaries' byte sequences, this may take a while...")
9294
tags, alo, ahi, blo, bhi = byte_differences(bin1.rawbytes, bin2.rawbytes)
93-
lg.debug("Plotting binaries...")
95+
logger.debug("> plotting binaries")
9496
text_x = -0.012*max(bin1.size*(len(legend1)+3), bin2.size*(len(legend2)+3))
9597
for i, d in enumerate([(bin1, zip(tags, alo, ahi), legend1), (bin2, zip(tags, blo, bhi), legend2)]):
9698
binary, opcodes, label = d
@@ -129,6 +131,8 @@ def byte_differences(bytes1, bytes2):
129131
if len(sections) == 0:
130132
obj.text(.5, ref_point, "Could not parse sections", fontsize=fs_ref*1.6, color="red", ha="center",
131133
va="center")
134+
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
135+
logger.debug("> configuring the figure")
132136
cb = plt.colorbar(ScalarMappable(cmap=ListedColormap(colors, N=4)),
133137
location='bottom', ax=objs[-1], fraction=0.3, aspect=50, ticks=[0.125, 0.375, 0.625, 0.875])
134138
cb.set_ticklabels(['removed', 'modified', 'untouched', 'added'])

src/exeplot/plots/entropy.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
from .__common__ import mean, Binary, COLORS, MIN_ZONE_WIDTH, N_SAMPLES, SUBLABELS
3-
from ..__conf__ import save_figure
3+
from ..__conf__ import *
44
from ..utils import shannon_entropy
55

66

@@ -25,7 +25,7 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
2525
:param n_samples: number of samples of entropy required
2626
:param window_size: window size for computing the entropy
2727
"""
28-
binary = Binary(executable)
28+
binary = executable if isinstance(executable, Binary) else Binary(executable)
2929
data = {'hash': binary.hash, 'name': binary.basename, 'size': binary.size, 'type': binary.type,
3030
'entropy': [], 'sections': []}
3131
# compute window-based entropy
@@ -94,6 +94,7 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
9494
from os import fstat
9595
if len(filenames) == 0:
9696
raise ValueError("No executable to plot")
97+
# ------------------------------------------------- DRAW THE PLOT --------------------------------------------------
9798
lloc, title = kwargs.get('legend_location', "lower center"), not kwargs.get('no_title', False)
9899
lloc_side = lloc.split()[1] in ["left", "right"]
99100
nf, N_TOP, N_TOP2, N_BOT, N_BOT2 = len(filenames), 1.15, 1.37, -.15, -.37
@@ -103,12 +104,13 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
103104
(objs[0] if nf+[0, 1][title] > 1 else objs).axis("off")
104105
ref_size, ref_n, fs_ref = None, kwargs.get('n_samples', N_SAMPLES), kwargs['config']['font_size']
105106
for i, filepath in enumerate(filenames):
107+
logger.debug(f"> plotting binary '{filepath}'")
108+
binary = Executable(filepath)
106109
if scale and ref_size:
107-
with open(filepath, "rb") as f:
108-
size = fstat(f.fileno()).st_size
109-
kwargs['n_samples'] = int(ref_n * size / ref_size)
110+
binary.rawbytes # triggers the computation of binary.__size
111+
kwargs['n_samples'] = int(ref_n * binary.size / ref_size)
110112
obj = objs[i+[0, 1][title]] if nf+[0, 1][title] > 1 else objs
111-
d = data(filepath, **kwargs)
113+
d = data(binary, **kwargs)
112114
n, label = len(d['entropy']), None
113115
if not ref_size:
114116
ref_size = d['size']
@@ -180,13 +182,16 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
180182
# draw entropy
181183
obj.plot(x, d['entropy'][start:end+1], c=c, zorder=10, lw=.1)
182184
obj.fill_between(x, [0] * len(x), d['entropy'][start:end+1], facecolor=c)
183-
l = obj.hlines(y=mean(d['entropy'][start:end+1]), xmin=x[0], xmax=x[-1], color="black", linewidth=.5, linestyle=(0, (5, 5)))
185+
l = obj.hlines(y=mean(d['entropy'][start:end+1]), xmin=x[0], xmax=x[-1], color="black", linewidth=.5,
186+
linestyle=(0, (5, 5)))
184187
i += 1
185188
if len(d['sections']) > 0:
186189
l.set_label("Average entropy of section")
187190
else:
188191
obj.text(.5, ref_point, "Could not parse sections", fontsize=fs_ref*1.6, color="red", ha="center",
189192
va="center")
193+
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
194+
logger.debug("> configuring the figure")
190195
plt.subplots_adjust(left=[.15, .02][labels is None and sublabel is None], right=[1.02, .82][lloc_side],
191196
bottom=.5/max(1.75, nf))
192197
h, l = (objs[[0, 1][title]] if nf+[0, 1][title] > 1 else objs).get_legend_handles_labels()

src/exeplot/plots/graph.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,11 @@
22
import matplotlib.pyplot as plt
33

44
from .__common__ import Binary, CACHE_DIR, COLORS, MIN_ZONE_WIDTH
5-
from ..__conf__ import check_imports, save_figure
6-
7-
try:
8-
# dirty fix to known issue with angr: AttributeError: module 'unicorn' has no attribute 'UC_ARCH_RISCV'.
9-
__import__("unicorn").UC_ARCH_RISCV = 8
10-
except ModuleNotFoundError: # pragma: no cover
11-
pass # 'unicorn' is an optional dependency of 'angr' ; fix it only if it is already installed
5+
from ..__conf__ import *
126

137
check_imports("angr", "networkx", "pygraphviz")
148

9+
1510
_DEFAULT_ALGORITHM, _DEFAULT_ENGINE = "fast", "default"
1611
_ENGINES = ["default", "pcode", "vex"]
1712

@@ -32,6 +27,8 @@ def plot(executable, algorithm=_DEFAULT_ALGORITHM, engine=_DEFAULT_ENGINE, **kwa
3227
import networkx as nx
3328
import pygraphviz as pgv
3429
from math import ceil, log2
30+
# ------------------------------------------------- DRAW THE PLOT --------------------------------------------------
31+
logger.debug("> computing the executable's CFG")
3532
engine = {k: getattr(angr.engines, "UberEngine" if k != "pcode" else f"UberEngine{k.capitalize()}") \
3633
for k in _ENGINES}[engine]
3734
project = angr.Project(executable, auto_load_libs=False, engine=engine)
@@ -40,6 +37,8 @@ def plot(executable, algorithm=_DEFAULT_ALGORITHM, engine=_DEFAULT_ENGINE, **kwa
4037
for node in cfg.graph.nodes():
4138
labels[node] = f"{node.name}\n0x{node.addr:x}" if hasattr(node, "name") and node.name else f"0x{node.addr:x}"
4239
node_colors.append("red" if node.function_address == node.addr else "lightblue")
40+
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
41+
logger.debug("> configuring the figure")
4342
n = max(10, min(30, ceil(log2(n_nodes := len(cfg.graph.nodes()) + 1) * 2)))
4443
fig = plt.figure(figsize=(n, n))
4544
nx.draw(cfg.graph, nx.kamada_kawai_layout(cfg.graph), fig.gca(), font_size=8, with_labels=True, labels=labels,

src/exeplot/plots/nested_pie.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
from .__common__ import Binary, COLORS, SHADOW
3-
from ..__conf__ import save_figure
3+
from ..__conf__ import *
44
from ..utils import human_readable_size
55

66

@@ -14,19 +14,24 @@ def plot(executable, **kwargs):
1414
""" draw a nested pie chart of segments (if relevant) and sections (including overlaps) of the input binary """
1515
import matplotlib.pyplot as plt
1616
from math import ceil
17+
# ------------------------------------------------- DRAW THE PLOT --------------------------------------------------
1718
binary = Binary(executable)
1819
fig, ax = plt.subplots(figsize=(6, 3), subplot_kw=dict(aspect="equal"))
1920
pie_kw = {'shadow': SHADOW} if kwargs['config']['shadow'] else {}
2021
seg_layers = sum(1 for _ in binary._data(segments=True, overlap=True))
2122
if binary.type != "PE":
23+
logger.debug("> computing pie segments")
2224
for i, x, w, labels, colors, legend in binary._data(segments=True, overlap=True):
2325
size = .4 / seg_layers
2426
ax.pie(w, radius=.45-i*size, colors=colors, startangle=90,
2527
wedgeprops={'width': size, 'edgecolor': "w", 'linewidth': 1}, **pie_kw)
28+
logger.debug("> computing pie sections")
2629
sec_layers = sum(1 for _ in binary._data(overlap=True))
2730
for i, x, w, labels, colors, legend in binary._data(overlap=True):
2831
size = .42 / sec_layers
2932
ax.pie(w, radius=1-i*size, colors=colors, startangle=90, wedgeprops={'width': size}, **pie_kw)
33+
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
34+
logger.debug("> configuring the figure")
3035
ncols = ceil(len(legend['colors']) / 12)
3136
ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
3237
bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(kwargs['config']['font_size']*.7))

src/exeplot/plots/pie.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
from .__common__ import Binary, COLORS, SHADOW
3-
from ..__conf__ import save_figure
3+
from ..__conf__ import *
44
from ..utils import human_readable_size
55

66

@@ -15,6 +15,8 @@ def plot(executable, donut=False, **kwargs):
1515
""" draw a pie chart of the sections of the input binary """
1616
import matplotlib.pyplot as plt
1717
from math import ceil
18+
# ------------------------------------------------- DRAW THE PLOT --------------------------------------------------
19+
logger.debug("> computing pie sections")
1820
fs_ref = kwargs['config']['font_size']
1921
binary = Binary(executable)
2022
fig, ax = plt.subplots(figsize=(6, 3), subplot_kw=dict(aspect="equal"))
@@ -27,6 +29,8 @@ def plot(executable, donut=False, **kwargs):
2729
pie_kw = {'shadow': SHADOW} if kwargs['config']['shadow'] else {}
2830
_, texts = ax.pie(data, colors=colors, labels=labels, textprops=txt_kw, labeldistance=.55, startangle=90,
2931
wedgeprops={'width': [1., .55][donut]}, **pie_kw)
32+
# ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
33+
logger.debug("> configuring the figure")
3034
ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
3135
bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(fs_ref*.7))
3236
plt.setp(texts, size=fs_ref*.8, weight="bold")

0 commit comments

Comments
 (0)