Commit ae122ee8 by devttys0

Initial move of hexdiff.py/plotter.py to modules.

parent 1a117715
import os
import binwalk.module
from binwalk.compat import *
from binwalk.common import BlockFile
class Plotter(object):
'''
Base class for plotting binaries in Qt.
Other plotter classes are derived from this.
'''
VIEW_DISTANCE = 1024
MAX_2D_PLOT_POINTS = 12500
MAX_3D_PLOT_POINTS = 25000
NAME = "Binary Visualization"
CLI = [
binwalk.module.ModuleOption(short='3',
long='3D',
kwargs={'axis' : 3, 'enabled' : True},
description='Generate a 3D binary visualization'),
binwalk.module.ModuleOption(short='2',
long='2D',
kwargs={'axis' : 2, 'enabled' : True},
description='Project data points onto 3D cube walls only'),
binwalk.module.ModuleOption(short='Z',
long='max-points',
type=int,
kwargs={'max_points' : 0},
nargs=1,
description='Set the maximum number of plotted data points'),
binwalk.module.ModuleOption(short='V',
long='show-grids',
kwargs={'show_grids' : True},
description='Display the x-y-z grids in the resulting plot'),
]
KWARGS = [
binwalk.module.ModuleKwarg(name='axis', default=3),
binwalk.module.ModuleKwarg(name='max_points', default=0),
binwalk.module.ModuleKwarg(name='show_grids', default=False),
]
def __init__(self, **kwargs):
'''
Class constructor.
@axis - Set to 2 for 2D plotting, 3 for 3D plotting.
@max_points - The maximum number of data points to display.
@show_grids - Set to True to display x-y-z grids.
Returns None.
'''
import pyqtgraph.opengl as gl
from pyqtgraph.Qt import QtGui
binwalk.module.process_kwargs(self, kwargs)
self.verbose = self.config.verbose
self.files = self.config.target_files
self.offset = self.config.offset
self.length = self.config.length
self.plane_count = -1
self.plot_points = None
if self.axis == 2:
self.MAX_PLOT_POINTS = self.MAX_2D_PLOT_POINTS
self._generate_data_point = self._generate_2d_data_point
elif self.axis == 3:
self.MAX_PLOT_POINTS = self.MAX_3D_PLOT_POINTS
self._generate_data_point = self._generate_3d_data_point
else:
raise Exception("Invalid Plotter axis specified: %d. Must be one of: [2, 3]." % self.axis)
if not self.max_points:
self.max_points = self.MAX_PLOT_POINTS
self.app = QtGui.QApplication([])
self.window = gl.GLViewWidget()
self.window.opts['distance'] = self.VIEW_DISTANCE
if len(self.files) == 1:
self.window.setWindowTitle(self.files[0])
def _print(self, message):
'''
Print console messages. For internal use only.
'''
if self.verbose:
print (message)
def _generate_plot_points(self, data_points):
'''
Generates plot points from a list of data points.
@data_points - A dictionary containing each unique point and its frequency of occurance.
Returns a set of plot points.
'''
total = 0
min_weight = 0
weightings = {}
plot_points = {}
# If the number of data points exceeds the maximum number of allowed data points, use a
# weighting system to eliminate data points that occur less freqently.
if sum(data_points.itervalues()) > self.max_points:
# First, generate a set of weight values 1 - 10
for i in range(1, 11):
weightings[i] = 0
# Go through every data point and how many times that point occurs
for (point, count) in iterator(data_points):
# For each data point, compare it to each remaining weight value
for w in get_keys(weightings):
# If the number of times this data point occurred is >= the weight value,
# then increment the weight value. Since weight values are ordered lowest
# to highest, this means that more frequent data points also increment lower
# weight values. Thus, the more high-frequency data points there are, the
# more lower-frequency data points are eliminated.
if count >= w:
weightings[w] += 1
else:
break
# Throw out weight values that exceed the maximum number of data points
if weightings[w] > self.max_points:
del weightings[w]
# If there's only one weight value left, no sense in continuing the loop...
if len(weightings) == 1:
break
# The least weighted value is our minimum weight
min_weight = min(weightings)
# Get rid of all data points that occur less frequently than our minimum weight
for point in get_keys(data_points):
if data_points[point] < min_weight:
del data_points[point]
for point in sorted(data_points, key=data_points.get, reverse=True):
plot_points[point] = data_points[point]
total += 1
if total >= self.max_points:
break
return plot_points
def _generate_data_point(self, data):
'''
Subclasses must override this to return the appropriate data point.
@data - A string of data self.axis in length.
Returns a data point tuple.
'''
return (0,0,0)
def _generate_data_points(self, file_name):
'''
Generates a dictionary of data points and their frequency of occurrance.
@file_name - The file to generate data points from.
Returns a dictionary.
'''
i = 0
data_points = {}
self._print("Generating data points for %s" % file_name)
with BlockFile(file_name, 'r', offset=self.offset, length=self.length) as fp:
fp.MAX_TRAILING_SIZE = 0
while True:
(data, dlen) = fp.read_block()
if not data or not dlen:
break
i = 0
while (i+(self.axis-1)) < dlen:
point = self._generate_data_point(data[i:i+self.axis])
if has_key(data_points, point):
data_points[point] += 1
else:
data_points[point] = 1
i += 3
return data_points
def _generate_plot(self, plot_points):
import numpy as np
import pyqtgraph.opengl as gl
nitems = float(len(plot_points))
pos = np.empty((nitems, 3))
size = np.empty((nitems))
color = np.empty((nitems, 4))
i = 0
for (point, weight) in iterator(plot_points):
r = 0.0
g = 0.0
b = 0.0
pos[i] = point
frequency_percentage = (weight / nitems)
# Give points that occur more frequently a brighter color and larger point size.
# Frequency is determined as a percentage of total unique data points.
if frequency_percentage > .005:
size[i] = .20
r = 1.0
elif frequency_percentage > .002:
size[i] = .10
g = 1.0
r = 1.0
else:
size[i] = .05
g = 1.0
color[i] = (r, g, b, 1.0)
i += 1
scatter_plot = gl.GLScatterPlotItem(pos=pos, size=size, color=color, pxMode=False)
scatter_plot.translate(-127.5, -127.5, -127.5)
return scatter_plot
def plot(self, wait=True):
import pyqtgraph.opengl as gl
self.window.show()
if self.show_grids:
xgrid = gl.GLGridItem()
ygrid = gl.GLGridItem()
zgrid = gl.GLGridItem()
self.window.addItem(xgrid)
self.window.addItem(ygrid)
self.window.addItem(zgrid)
# Rotate x and y grids to face the correct direction
xgrid.rotate(90, 0, 1, 0)
ygrid.rotate(90, 1, 0, 0)
# Scale grids to the appropriate dimensions
xgrid.scale(12.8, 12.8, 12.8)
ygrid.scale(12.8, 12.8, 12.8)
zgrid.scale(12.8, 12.8, 12.8)
for file_name in self.files:
data_points = self._generate_data_points(file_name)
self._print("Generating plot points from %d data points" % len(data_points))
self.plot_points = self._generate_plot_points(data_points)
del data_points
self._print("Generating graph from %d plot points" % len(self.plot_points))
self.window.addItem(self._generate_plot(self.plot_points))
if wait:
self.wait()
def wait(self):
from pyqtgraph.Qt import QtCore, QtGui
t = QtCore.QTimer()
t.start(50)
QtGui.QApplication.instance().exec_()
def _generate_3d_data_point(self, data):
'''
Plot data points within a 3D cube.
'''
return (ord(data[0]), ord(data[1]), ord(data[2]))
def _generate_2d_data_point(self, data):
'''
Plot data points projected on each cube face.
'''
self.plane_count += 1
if self.plane_count > 5:
self.plane_count = 0
if self.plane_count == 0:
return (0, ord(data[0]), ord(data[1]))
elif self.plane_count == 1:
return (ord(data[0]), 0, ord(data[1]))
elif self.plane_count == 2:
return (ord(data[0]), ord(data[1]), 0)
elif self.plane_count == 3:
return (255, ord(data[0]), ord(data[1]))
elif self.plane_count == 4:
return (ord(data[0]), 255, ord(data[1]))
elif self.plane_count == 5:
return (ord(data[0]), ord(data[1]), 255)
def run(self):
self.plot()
return self.plot_points
#!/usr/bin/env python #!/usr/bin/env python
# TODO: Use sane defaults for block size and file size, if not specified.
# Handle header output for multiple files.
import os import os
import sys import sys
import curses import curses
import platform import platform
import binwalk.module
import binwalk.common as common import binwalk.common as common
from binwalk.compat import * from binwalk.compat import *
...@@ -22,23 +26,43 @@ class HexDiff(object): ...@@ -22,23 +26,43 @@ class HexDiff(object):
'blue' : '34', 'blue' : '34',
} }
def __init__(self, binwalk=None): NAME = "Binary Diffing"
CLI = [
binwalk.module.ModuleOption(short='W',
long='hexdump',
kwargs={'enabled' : True},
description='Perform a hexdump / diff of a file or files'),
binwalk.module.ModuleOption(short='G',
long='green',
kwargs={'show_green' : True, 'show_blue' : False, 'show_green' : False},
description='Only show lines containing bytes that are the same among all files'),
binwalk.module.ModuleOption(short='i',
long='red',
kwargs={'show_red' : True, 'show_blue' : False, 'show_green' : False},
description='Only show lines containing bytes that are different among all files'),
binwalk.module.ModuleOption(short='U',
long='blue',
kwargs={'show_blue' : True, 'show_red' : False, 'show_green' : False},
description='Only show lines containing bytes that are different among some files'),
binwalk.module.ModuleOption(short='w',
long='terse',
kwargs={'terse' : True},
description='Diff all files, but only display a hex dump of the first file'),
]
KWARGS = [
binwalk.module.ModuleKwarg(name='show_red', default=True),
binwalk.module.ModuleKwarg(name='show_blue', default=True),
binwalk.module.ModuleKwarg(name='show_green', default=True),
binwalk.module.ModuleKwarg(name='terse', default=False),
]
def __init__(self, **kwargs):
binwalk.module.process_kwargs(self, kwargs)
self.block_hex = "" self.block_hex = ""
self.printed_alt_text = False self.printed_alt_text = False
if binwalk:
self._pprint = binwalk.display._pprint
self._show_header = binwalk.display.header
self._footer = binwalk.display.footer
self._display_result = binwalk.display.results
self._grep = binwalk.filter.grep
else:
self._pprint = sys.stdout.write
self._show_header = self._print
self._footer = self._simple_footer
self._display_result = self._print
self._grep = None
if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() and platform.system() != 'Windows': if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() and platform.system() != 'Windows':
curses.setupterm() curses.setupterm()
self.colorize = self._colorize self.colorize = self._colorize
...@@ -57,15 +81,28 @@ class HexDiff(object): ...@@ -57,15 +81,28 @@ class HexDiff(object):
return "\x1b[%sm%s\x1b[0m" % (';'.join(attr), c) return "\x1b[%sm%s\x1b[0m" % (';'.join(attr), c)
def _color_filter(self, data):
red = '\x1b[' + self.COLORS['red'] + ';'
green = '\x1b[' + self.COLORS['green'] + ';'
blue = '\x1b[' + self.COLORS['blue'] + ';'
if self.show_blue and blue in data:
return True
if self.show_green and green in data:
return True
if self.show_red and red in data:
return True
return False
def _print_block_hex(self, alt_text="*"): def _print_block_hex(self, alt_text="*"):
printed = False printed = False
if self._grep is None or self._grep(self.block_hex): if self._color_filter(self.block_hex):
self._pprint(self.block_hex) self.config.display.result(self.block_hex)
self.printed_alt_text = False self.printed_alt_text = False
printed = True printed = True
elif not self.printed_alt_text: elif not self.printed_alt_text:
self._pprint("%s\n" % alt_text) self.config.display.result("%s\n" % alt_text)
self.printed_alt_text = True self.printed_alt_text = True
printed = True printed = True
...@@ -82,33 +119,29 @@ class HexDiff(object): ...@@ -82,33 +119,29 @@ class HexDiff(object):
else: else:
self.block_hex += c self.block_hex += c
def _simple_footer(self): def run(self):
print("")
def _header(self, files, block):
header = "OFFSET "
for i in range(0, len(files)):
f = files[i]
header += "%s" % os.path.basename(f)
if i != len(files)-1:
header += " " * ((block*4) + 10 - len(os.path.basename(f)))
self._show_header(header=header)
def display(self, files, offset=0, size=DEFAULT_DIFF_SIZE, block=DEFAULT_BLOCK_SIZE, show_first_only=False):
i = 0 i = 0
total = 0 total = 0
fps = [] fps = []
data = {} data = {}
delim = '/' delim = '/'
offset = self.config.offset
size = self.config.length
block = self.config.block
files = self.config.target_files
self.config.display.format_strings("\n%s\n", "%s\n")
# If negative offset, then we're going that far back from the end of the file # If negative offset, then we're going that far back from the end of the file
if offset < 0: if offset < 0:
size = offset * -1 size = offset * -1
if show_first_only: # TODO: Display all file names in hexdump
self._header([files[0]], block) if self.terse:
self.config.display.header(files[0])
else: else:
self._header(files, block) self.config.display.header(files[0])
if common.BlockFile.READ_BLOCK_SIZE < block: if common.BlockFile.READ_BLOCK_SIZE < block:
read_block_size = block read_block_size = block
...@@ -169,7 +202,7 @@ class HexDiff(object): ...@@ -169,7 +202,7 @@ class HexDiff(object):
diff_same[j] = self.SOME_DIFF diff_same[j] = self.SOME_DIFF
for index in range(0, len(files)): for index in range(0, len(files)):
if show_first_only and index > 0: if self.terse and index > 0:
break break
f = files[index] f = files[index]
...@@ -194,7 +227,7 @@ class HexDiff(object): ...@@ -194,7 +227,7 @@ class HexDiff(object):
except: except:
self._build_block(' ') self._build_block(' ')
if index == len(files)-1 or (show_first_only and index == 0): if index == len(files)-1 or (self.terse and index == 0):
self._build_block("|\n") self._build_block("|\n")
else: else:
self._build_block('| %s ' % delim) self._build_block('| %s ' % delim)
...@@ -211,8 +244,7 @@ class HexDiff(object): ...@@ -211,8 +244,7 @@ class HexDiff(object):
for fp in fps: for fp in fps:
fp.close() fp.close()
self._footer() self.config.display.footer()
if __name__ == "__main__": return True
HexDiff().display(sys.argv[1:])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment