#!/usr/bin/python
# -*- coding: utf-8 -*-
#
#  IDAtropy
#  last update: 2018/02/06
#
#  Daniel Garcia <danigargu [at] gmail.com>
#  @danigargu
# 
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software  Foundation, either  version 3 of  the License, or
#  (at your option) any later version.
#

import sys
import math
import zlib
import string
import random

from idc import *
from idaapi import *
from idautils import *
from sets import Set

from collections import Counter, OrderedDict

# IDA < 6.9 support
if IDA_SDK_VERSION < 690:
  from PySide import QtGui, QtCore
  from PySide.QtGui import QTextEdit, QTableWidget, QTreeWidget, QCheckBox
  QtWidgets = QtGui
  USE_PYQT5 = False
else:
  from PyQt5 import QtGui, QtCore, QtWidgets
  from PyQt5.QtWidgets import QTextEdit, QTableWidget, QTreeWidget, QCheckBox
  USE_PYQT5 = True

try:
  import sip
  import matplotlib
  import matplotlib.pyplot as plt
  import matplotlib.ticker as ticker
  from matplotlib.colors import hsv_to_rgb

  if USE_PYQT5:
    matplotlib.use('Qt5Agg')
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
  else:
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
  from matplotlib.backend_bases import key_press_handler  
except ImportError:
  ERROR_MATPLOTLIB = True


PLUG_NAME    = "IDAtropy"
PLUG_VERSION = "v0.3"


def log(msg):
  Message("[%s] %s\n" % (PLUG_NAME, msg))

def histogram(data):
  table = [0]*256
  for i in map(ord, data):
    table[i] += 1
  return table

def gen_rand_colors(n_colors=20):
  """ https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ """
  colors = []
  golden_ratio_conjugate = 0.618033988749895
  h = random.random()

  for i in range(n_colors):
    h += golden_ratio_conjugate
    h %= 1
    values = hsv_to_rgb([h, 0.3, 0.95])
    res = "#" + ''.join(["%02X" % int(val*256) for val in values])
    colors.append(res)

  return colors

def entropy_scan(data, block_size=256, step_size=1) :
  for block in (data[x:block_size+x] for x in xrange (0, len(data)-block_size, step_size)):
    yield entropy(block)

def entropy(data):
    """Calculate the entropy of a chunk of data."""

    if len(data) == 0:
        return 0.0

    occurences = Counter(bytearray(data))
    entropy = 0
    for x in occurences.values():
        p_x = float(x) / len(data)
        entropy -= p_x*math.log(p_x, 2)

    return entropy

def calc_compression_ratio(data):
  comp = zlib.compress(data)
  result = (float(len(comp))/len(data))*100
  if result > 100:
    result = 100
  return result

def get_loaded_bytes(start_addr, size, fill_with="\x00"):
  bytes = ""
  cur_ea = start_addr
  while cur_ea < (start_addr+size):
    if isLoaded(cur_ea):
      bytes += chr(get_byte(cur_ea))
    else:
      bytes += fill_with
    cur_ea += 1
  return bytes

def my_put_bytes(ea, buf):
  for i in xrange(len(buf)):
    patch_byte(ea+i, ord(buf[i]))

def get_disk_binary():
  data = None
  with open(get_input_file_path(), 'rb') as f:
    data = f.read()
  return data

def load_file_in_segment(filename, seg_name):
  last_seg = get_last_seg()
  seg_start = last_seg.endEA
  data = None

  with open(filename, 'rb') as f:
    data = f.read()

  seg_len = len(data)
  if seg_len % 0x1000 != 0:
    seg_len = seg_len + (0x1000 - (seg_len % 0x1000))

  if add_segm(0, seg_start, seg_start+seg_len, seg_name, "DATA"):
    put_bytes(seg_start, data)
    return True
  return False

class Config:
  chart_type   = 0
  start_addr   = 0
  end_addr     = 0
  use_disk_binary  = False
  use_debug_memory = False

  chart_types = ('Entropy','Histogram')

  xrefs = {
    "min_entropy": 6.8,
    "block_size": 256
  }

  entropy = {
    "all_segments": False,
    "block_size": 256,
    "step_size": 256,
    "segm_exists": False,
    "segm_addr": None,
    "segm_name": "IDAtropy"
  }

class ChartTypes:
  ENTROPY = 0
  HISTOGRAM = 1

class XrefsEntropy(Choose2):
  def __init__(self, title, config):
    Choose2.__init__(self, title, [ 
        ["Address", 16 | Choose2.CHCOL_HEX],
        ["Entropy", 10 | Choose2.CHCOL_DEC],
        ["Xrefs", 4 | Choose2.CHCOL_HEX],         
        ["Is code", 4 | Choose2.CHCOL_DEC],
        ["Xref Type", 15 | Choose2.CHCOL_PLAIN] ])

    self.title  = title
    self.items  = []
    self.icon   = 55 # xref icon
    self.config = config 
    self.PopulateItems()

  def OnClose(self):
    return True

  def OnSelectLine(self, n):
    item = self.items[int(n)]
    jumpto(int(item[0], 16))

  def OnGetLine(self, index):
    return self.items[index]

  def OnGetSize(self):
    return len(self.items)

  def OnDeleteLine(self, n):
    del self.items[n]
    return n

  def OnGetLineAttr(self,n):
    pass

  def OnRefresh(self, n):
    return n

  def OnCommand(self, n, cmd_id):
    if cmd_id == self.cmd_exclude_code_xrefs:
      self.exclude_code_xrefs()  
    return n

  def exclude_code_xrefs(self):
    if not len(self.items):
      return False 

    self.items = [i for i in self.items if i[3] != '1']

    if IDA_SDK_VERSION >= 700:
      refresh_choosers()

    return True

  def show(self):
    n_items = len(self.items)
    if n_items > 0:
      b = self.Show()
      if b == 0:
        self.cmd_exclude_code_xrefs = self.AddCommand("Exclude code xrefs")
        return True
    else:
      warning("No xrefs found")
    return False

  def PopulateItems(self):
    min_entropy = self.config['min_entropy']
    cur_ea = self.config['start_addr']

    show_wait_box("Searching xrefs...")

    while cur_ea < self.config['end_addr']:
      xrefs = list(XrefsTo(cur_ea))
      if len(xrefs) > 0 and xrefs[0].type != fl_F: # discard ordinary flow
        bytes = get_bytes(cur_ea, self.config['block_size'])
        assert len(bytes) == self.config['block_size']

        ent = entropy(bytes)
        if ent >= min_entropy:
          self.items.append([
            "%08X" % cur_ea,
            "%.04f" % ent,
            "%d" % len(xrefs),
            "%d" % xrefs[0].iscode,
            "%s" % XrefTypeName(xrefs[0].type)              
          ])
      cur_ea += 1

    hide_wait_box()


class Options(QtWidgets.QWidget):
  def __init__(self, parent):
    QtWidgets.QWidget.__init__(self)
    self.parent = parent
    self.config = parent.config
    self.name = "Options"
    self.check_if_segm_exists()
    self.create_gui()

  def check_if_segm_exists(self):
    segm = get_segm_by_name(self.config.entropy['segm_name'])
    if segm:
      self.config.entropy['segm_exists'] = True
      self.config.entropy['segm_addr']   = segm.startEA
    else:
      self.config.entropy['segm_exists'] = False
      self.config.entropy['segm_addr']   = None

  def button_chart_on_click(self):
    try:      
      show_wait_box("Making chart...")
      tab_title = self.get_tab_title()

      if self.config.chart_type == ChartTypes.ENTROPY:
        if self.config.use_disk_binary and not self.config.entropy['segm_exists']:

          msg1 = "Do you want to create new segment with the binary content?\n"
          msg2 = "This will allow you to navigate over the file by double-clicking on the chart"
          if askyn_c(1, "HIDECANCEL\n" + msg1 + msg2) == 1:
            self.create_segment_with_binary()

        self.parent.tabs.addTab(Entropy(self), tab_title)

      elif self.config.chart_type == ChartTypes.HISTOGRAM:
        self.parent.tabs.addTab(Histogram(self), tab_title)
      
    except Exception, e:
      warning("%s" % traceback.format_exc())
    hide_wait_box()

  def button_xrefs_on_click(self):
    log("Start address : 0x%08x" % self.config.start_addr)
    log("End address : 0x%08x" % self.config.end_addr)

    xrefs_config = {
      "start_addr": self.config.start_addr,
      "end_addr": self.config.end_addr,
      "block_size": self.config.xrefs['block_size'],
      "min_entropy": float(self.t_min_entropy.text()),
    }    
    choose = XrefsEntropy("%s - XrefsTo" % PLUG_NAME, xrefs_config)
    choose.show()

  def create_segment_with_binary(self):
    segm_name = self.config.entropy['segm_name']
    if load_file_in_segment(GetInputFilePath(), segm_name):
      self.config.segm_exists = True
      self.check_if_segm_exists()

  def get_chart_type_str(self):
    return self.config.chart_types[self.config.chart_type]

  def get_tab_title(self):
    title = self.get_chart_type_str() + " - "
    if self.config.use_disk_binary:
      title += "Disk binary"
    elif self.config.entropy['all_segments']:
      title += "All segments"
    else:
      segname = SegName(self.config.start_addr)
      if segname:
        title += "%s " % segname
      title += "[0x%08x - 0x%08x]" % (self.config.start_addr, self.config.end_addr)
    return title

  def get_data(self):
    data = None
    if self.config.use_disk_binary:
      data = get_disk_binary()
    else:
      data_size = self.config.end_addr - self.config.start_addr
      data = get_bytes(self.config.start_addr, data_size)
    return data

  def update_progress_bars(self):
      data = self.get_data()
      ent = entropy(data)
      norm_ent = ent/8*100
      comp_ratio = calc_compression_ratio(data)

      self.pb_entropy.setValue(norm_ent)
      self.pb_entropy.setFormat("%0.2f" % ent)
      self.pb_comp_ratio.setValue(comp_ratio)

  def update_address(self):
    sender = self.sender()
    value = sender.text()

    if len(value) == 0 or value == "0x":
      return

    try:
      value = int(value, 16)
      if sender is self.t_start_addr:
        self.config.start_addr = value
      elif sender is self.t_end_addr:
        self.config.end_addr = value

      """ update progress bars """

      """
      if self.config.start_addr and self.config.end_addr \
        and self.config.start_addr < self.config.end_addr:

        self.update_progress_bars()
      """

    except ValueError as e:
      warning("Invalid value for address")

  def update_entropy_config(self):
    try:
      block_size = int(self.t_block_size.text())
      step_size = int(self.t_step_size.text())

      self.slider_block_s.setValue(block_size)
      self.slider_step_s.setValue(step_size)
      self.slider_step_s.setMaximum(block_size)

      self.config.entropy['block_size'] = block_size
      self.config.entropy['step_size'] = step_size
    except ValueError:
      log("Invalid value")
    
  def create_gui(self):
    lbl_start_address = QtWidgets.QLabel("Start address")
    self.t_start_addr = QtWidgets.QLineEdit()
    self.t_start_addr.setFixedWidth(200)

    lbl_end_address = QtWidgets.QLabel("End address")
    self.t_end_addr = QtWidgets.QLineEdit()
    self.t_end_addr.setFixedWidth(200)
    
    lbl_segment = QtWidgets.QLabel("Segment")
    self.cb_segment = QtWidgets.QComboBox()
    self.cb_segment.setFixedWidth(200)

    lbl_disk_binary = QtWidgets.QLabel("Use disk binary")
    self.cb_disk_bin = QtWidgets.QCheckBox()

    lbl_chart_type = QtWidgets.QLabel("Chart type")
    chart_types_group = self.create_chart_type_group()

    """ chunk size """
    ent_config = self.config.entropy

    lbl_block_size = QtWidgets.QLabel("Block size")
    lbl_block_size.setFixedWidth(60)
    self.t_block_size = QtWidgets.QLineEdit()
    self.t_block_size.setFixedWidth(40)
    self.t_block_size.setText("%d" % ent_config['block_size'])
    self.t_block_size.setToolTip("Size of the data blocks to calculate entropy")

    self.slider_block_s = QtWidgets.QSlider(QtCore.Qt.Horizontal)
    self.slider_block_s.setFixedWidth(70)
    self.slider_block_s.setMinimum(256)
    self.slider_block_s.setMaximum(4096)
    self.slider_block_s.setValue(ent_config['block_size'])
    self.slider_block_s.setSingleStep(8)

    hbox_block_s = QtWidgets.QHBoxLayout()
    hbox_block_s.addWidget(lbl_block_size)
    hbox_block_s.addWidget(self.t_block_size)
    hbox_block_s.addWidget(self.slider_block_s)
    
    """ step size """
    lbl_step_size = QtWidgets.QLabel("Step size")
    lbl_step_size.setFixedWidth(60)
    self.t_step_size = QtWidgets.QLineEdit()
    self.t_step_size.setFixedWidth(40)
    self.t_step_size.setText("%d" % ent_config['step_size'])

    msg1  = "Displacement in bytes between each iteration of entropy.\n"
    msg1 += "The step used must be less than or equal to the block size.\n\n"
    msg1 += "For example, 1 will get a list of entropies for all data offsets.\n"
    msg1 += "Step sizes greater will have less precision and will be normalized\n"
    msg1 += "in the mouse events on the chart."

    self.t_step_size.setToolTip(msg1)

    self.slider_step_s = QtWidgets.QSlider(QtCore.Qt.Horizontal)
    self.slider_step_s.setFixedWidth(70)
    self.slider_step_s.setMinimum(1)
    self.slider_step_s.setMaximum(ent_config['block_size'])
    self.slider_step_s.setValue(ent_config['step_size'])

    hbox_step = QtWidgets.QHBoxLayout()
    hbox_step.addWidget(lbl_step_size)
    hbox_step.addWidget(self.t_step_size)
    hbox_step.addWidget(self.slider_step_s)

    """ All segments """
    lbl_all_segments = QtWidgets.QLabel("All segments")
    self.cb_all_segments = QtWidgets.QCheckBox()

    """ entropy config """
    form_entropy =  QtWidgets.QFormLayout()
    form_entropy.addRow(lbl_all_segments, self.cb_all_segments)
    form_entropy.addRow(hbox_block_s)
    form_entropy.addRow(hbox_step)
    self.groupbox_entropy = QtWidgets.QGroupBox('Entropy chart config')
    self.groupbox_entropy.setLayout(form_entropy)
    self.groupbox_entropy.setFixedWidth(300)

    lbl_min_entropy = QtWidgets.QLabel("Min entropy")
    lbl_block_s_xref = QtWidgets.QLabel("Block size")
    self.t_min_entropy = QtWidgets.QLineEdit()
    self.t_block_s_xrefs = QtWidgets.QLineEdit()
    self.t_min_entropy.setFixedWidth(80)
    self.t_block_s_xrefs.setFixedWidth(80)
    self.t_min_entropy.setText("%.02f" % self.config.xrefs['min_entropy'])
    self.t_block_s_xrefs.setText("%d" % self.config.xrefs['block_size'])

    form_xrefs = QtWidgets.QFormLayout()
    form_xrefs.addRow(lbl_min_entropy, self.t_min_entropy)
    form_xrefs.addRow(lbl_block_s_xref, self.t_block_s_xrefs)

    groupbox_xrefs = QtWidgets.QGroupBox('Xrefs finder')
    groupbox_xrefs.setLayout(form_xrefs)
    groupbox_xrefs.setFixedWidth(300)

    """ progress bars """
    progress_bar_style = """
    QProgressBar {
        border: 1px solid grey;
        border-radius: 2px;
        text-align: center;
    }

    QProgressBar::chunk {
        background-color: #BDE99F;
        width: 4px;
    }"""

    lbl_compress_ratio = QtWidgets.QLabel("Compression ratio") 
    self.pb_comp_ratio = QtWidgets.QProgressBar()
    self.pb_comp_ratio.setStyleSheet(progress_bar_style)
    self.pb_comp_ratio.setFixedWidth(200)

    lbl_entropy = QtWidgets.QLabel("Shannon entropy") 
    self.pb_entropy = QtWidgets.QProgressBar()
    self.pb_entropy.setStyleSheet(progress_bar_style)
    self.pb_entropy.setFixedWidth(200)

    """ buttons """
    self.button_chart = QtWidgets.QPushButton("Draw chart")
    self.button_chart.setFixedWidth(100)

    self.button_xrefs = QtWidgets.QPushButton("Find Xrefs")
    self.button_xrefs.setFixedWidth(100)

    hbox_buttons = QtWidgets.QHBoxLayout()
    hbox_buttons.addWidget(self.button_chart)
    hbox_buttons.addWidget(self.button_xrefs)

    """ main form """
    main_form = QtWidgets.QFormLayout()
    main_form.addRow(lbl_start_address, self.t_start_addr)
    main_form.addRow(lbl_end_address, self.t_end_addr)
    main_form.addRow(lbl_segment, self.cb_segment)
    main_form.addRow(lbl_compress_ratio, self.pb_comp_ratio)
    main_form.addRow(lbl_entropy, self.pb_entropy)
    main_form.addRow(lbl_disk_binary, self.cb_disk_bin)
    main_form.addRow(lbl_chart_type, chart_types_group)
    main_form.addRow(self.groupbox_entropy)
    main_form.addRow(groupbox_xrefs)  
    main_form.addRow(hbox_buttons)
    main_form.setAlignment(QtCore.Qt.AlignLeft)

    """ signals """
    self.cb_segment.currentIndexChanged[int].connect(self.cb_segment_changed)
    self.cb_all_segments.stateChanged.connect(self.cb_changed)
    self.cb_disk_bin.stateChanged.connect(self.cb_changed)
    self.button_chart.clicked.connect(self.button_chart_on_click)
    self.button_xrefs.clicked.connect(self.button_xrefs_on_click)

    for widget in [self.slider_block_s, self.slider_step_s]:
      widget.valueChanged.connect(self.slider_valuechanged)

    for widget in [self.t_start_addr, self.t_end_addr]:
      widget.textChanged.connect(self.update_address)

    for widget in [self.t_block_size, self.t_step_size]:
      widget.textChanged.connect(self.update_entropy_config)

    self.fill_segments()
    self.setLayout(main_form)

  def slider_valuechanged(self):
    sender = self.sender()
    value = int(sender.value())

    if sender is self.slider_step_s:
      self.t_step_size.setText("%d" % value)
      self.config.entropy['step_size']  = value

    elif sender is self.slider_block_s:      
      self.t_block_size.setText("%d" % value)
      self.config.entropy['block_size'] = value

  def cb_changed(self, state):
    sender = self.sender()

    if sender is self.cb_disk_bin:
      checked = (state == QtCore.Qt.Checked)  
      self.config.use_disk_binary = checked
      b_enabled = not checked

      self.update_progress_bars()

      self.t_start_addr.setEnabled(b_enabled)
      self.t_end_addr.setEnabled(b_enabled)
      self.cb_segment.setEnabled(b_enabled)
      self.cb_all_segments.setEnabled(b_enabled)

    elif sender is self.cb_all_segments:
      checked = (state == QtCore.Qt.Checked)
      self.config.entropy['all_segments'] = checked
      self.cb_disk_bin.setEnabled(not checked)

  def fill_segments(self):
    segments = filter(self.segment_filter, Segments())

    for idx, s_ea in enumerate(segments):
      if idx == 0:
        self.set_address(SegStart(s_ea), SegEnd(s_ea))
      self.cb_segment.addItem(SegName(s_ea), s_ea)

    if not segments:
      self.set_address(MinEA(), MaxEA())
      self.cb_segment.setEnabled(False)

  def create_chart_type_group(self):
    vbox = QtWidgets.QVBoxLayout()
    self.rg_chart_type = QtWidgets.QButtonGroup()
    self.rg_chart_type.setExclusive(True)

    for i, choice in enumerate(self.config.chart_types):
      radio = QtWidgets.QRadioButton(choice)
      self.rg_chart_type.addButton(radio, i)
      if i == self.config.chart_type: 
        radio.setChecked(True)
      vbox.addWidget(radio)

    vbox.addStretch(1)
    self.rg_chart_type.buttonClicked.connect(self.bg_graph_type_changed)
    return vbox

  def bg_graph_type_changed(self, radio):
    selected_id = self.rg_chart_type.checkedId()
    self.config.chart_type = selected_id

    b_enabled = (self.get_chart_type_str() == 'Entropy')
    self.groupbox_entropy.setEnabled(b_enabled)

  def cb_segment_changed(self, value):
    s_ea = self.sender().itemData(value)
    self.set_address(SegStart(s_ea), SegEnd(s_ea))
    self.update_progress_bars()   

  def set_address(self, start_addr, end_addr):
    self.t_start_addr.setText("0x%x" % start_addr)
    self.t_end_addr.setText("0x%x" % end_addr)

  def segment_filter(self, s_ea):
    """ Discard extern segments """
    if GetSegmentAttr(s_ea, SEGATTR_TYPE) != SEG_XTRN and \
     SegName(s_ea) != self.config.entropy['segm_name']:
      return True
    return False


class Entropy(QtWidgets.QWidget):
  def __init__(self, parent):
    QtWidgets.QWidget.__init__(self)
    self.parent         = parent
    self.config         = parent.config
    self.entropy_cfg    = self.config.entropy
    self.segments       = None
    self.data           = None
    self.data_size      = None
    self.calc_addr_fcn  = None
    self.make_chart()

  def make_chart(self):
    if self.entropy_cfg['all_segments']:
      self.get_segments_memory()
      self.make_segments_chart()
    else:
      self.get_data()
      self.make_normal_chart()

  def segment_filter(self, s_ea):
    """ Discard extern segments """
    if GetSegmentAttr(s_ea, SEGATTR_TYPE) != SEG_XTRN and \
     SegName(s_ea) != self.config.entropy['segm_name']:
      return True
    return False

  def get_data(self):
    data = None
    if self.config.use_disk_binary:
      data = get_disk_binary()
    else:
      data_size = self.config.end_addr - self.config.start_addr
      data = get_bytes(self.config.start_addr, data_size)

    self.data = data
    self.data_size = len(data)

  def format_coord_segments(self, x, y):
    try:
      addr = self.calc_addr_fcn(int(x))
      return "0x%08X - %-20s" % (addr, SegName(addr))
    except:
      return 'bad address'

  def get_segments_memory(self):    
    memory = ""
    step_size = self.entropy_cfg['step_size']
    segments  = OrderedDict()

    for ea in filter(self.segment_filter, Segments()):
      seg_name = SegName(ea)
      segm  = get_segm_by_name(seg_name)
      bytes = get_bytes(segm.startEA, segm.size())
      assert len(bytes) == segm.size()

      start_offset = len(memory)
      end_offset   = (start_offset+len(bytes))

      seg_info = {
        'segm': segm,
        'entropy': entropy(bytes),
        'offsets': [
          start_offset ,
          end_offset
        ],
        'chart_offsets': [
          start_offset / step_size, 
          end_offset / step_size
        ]
      }
      segments[seg_name] = seg_info
      memory += bytes

    self.data = memory
    self.data_size = len(memory)
    self.segments = segments

  def calc_point_addr_segments(self, x):
    addr = None
    for segm_name, segm_info in self.segments.iteritems():
      start, end = segm_info['chart_offsets']
      if start <= x < end:
        norm_x = (x-start) * self.entropy_cfg['step_size']
        addr = segm_info['segm'].startEA + norm_x
        break
    return addr

  def calc_point_addr_normal(self, x):
    addr = None
    offset = x * self.config.entropy['step_size']
    if self.config.use_disk_binary and self.config.entropy['segm_exists']:
      addr = self.config.entropy['segm_addr'] + offset
    else:
      addr = self.config.start_addr + offset
    return addr

  def segment_changed(self, item):
      row = item.row()
      col = item.column()
      seg_name = item.text()

      if (item.checkState() == QtCore.Qt.Checked):
        start, end = self.segments[seg_name]['chart_offsets']
        aspan = plt.axvspan(start, end, color=self.colors[row % len(self.colors)], alpha=0.6)
        self.spans[seg_name] = aspan      
      else:
        if seg_name in self.spans.keys():
          self.spans[seg_name].remove()
          del self.spans[seg_name]      
      self.canvas.draw()

  def make_segments_chart(self):    
    segment_names = []
    x_axis = []

    self.calc_addr_fcn = self.calc_point_addr_segments

    for segm_name, seg_info in self.segments.iteritems():
      segment_names.append(segm_name)
      x_axis.append(seg_info['chart_offsets'][0])

    x_limit = self.data_size / self.entropy_cfg['step_size']
    self.colors = gen_rand_colors(25)

    self.spans = dict()
    results = list(entropy_scan(self.data, 
      self.config.entropy['block_size'], 
      self.config.entropy['step_size'])
    )

    blocks = len(results)
    min_value, max_value  = min(results), max(results)
    avg_values = sum(results) / len(results)

    #plt.rc('xtick', labelsize=6)
    #plt.rc('ytick', labelsize=6)

    self.fig = plt.figure(facecolor='white')
    ax = plt.subplot(111, facecolor='white')

    ax.set_xlabel("byte range / blocks")
    ax.set_ylabel("Entropy (E)")

    ax2 = ax.twiny()
    ax2.set_xlabel("Segments")
    ax2.set_xlim(0, x_limit)
    ax2.set_xticks(x_axis)

    labels = ax2.set_xticklabels(segment_names, rotation=40)
    ax.axis([0, blocks, 0, 8])

    ax2.format_coord = self.format_coord_segments
    plt.plot(results, color="#2E9AFE")
    plt.tight_layout()
   
    log("Entropy - Start address: 0x%08x" % self.config.start_addr)
    log("Entropy - End address:   0x%08x" % self.config.end_addr)
    log("Entropy - Data size: %d bytes (blocks: %d)" % (len(self.data), blocks))
    info_str = 'Entropy - Min: %.2f | Max:  %.2f | Avg: %.2f' % (min_value, max_value, avg_values)
    log(info_str)

    del self.data

    self.canvas  = FigureCanvas(self.fig)
    self.toolbar = NavigationToolbar(self.canvas, self)

    self.cb_jump_on_click = QtWidgets.QCheckBox("Disable double-click event")    
    self.cb_jump_on_click.stateChanged.connect(self.disable_jump_on_click)

    """ segment table """
    self.segments_table = QtWidgets.QTableView()
    self.segments_table.setMaximumWidth(200)
    self.segments_table.setMaximumHeight(200)
    self.segments_table.verticalHeader().hide()

    model = QtGui.QStandardItemModel()
    model.setHorizontalHeaderLabels(['Segment','Entropy'])
    model.setHeaderData(0, QtCore.Qt.Horizontal, QtCore.Qt.AlignJustify, QtCore.Qt.TextAlignmentRole)
    
    self.segments_table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
    self.segments_table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
    self.segments_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
    self.segments_table.setModel(model)

    for n, seg_name in enumerate(segment_names):
      segment_name = QtGui.QStandardItem(seg_name)
      segment_entropy = QtGui.QStandardItem("%.03f" % self.segments[seg_name]['entropy'])
      model.setItem(n, 0, segment_name)
      model.setItem(n, 1, segment_entropy)
      segment_name.setCheckable(True)

    self.segments_table.horizontalHeader().setStretchLastSection(1);
    self.segments_table.resizeRowsToContents()
    self.segments_table.resizeColumnsToContents()
    self.segments_table.sortByColumn(1, QtCore.Qt.DescendingOrder)
    model.itemChanged.connect(self.segment_changed)
    
    lbl_segments = QtWidgets.QLabel("Select a segment to color it")
    vbox = QtWidgets.QVBoxLayout(self)
    vbox.addWidget(lbl_segments)
    vbox.addWidget(self.segments_table)

    group = QtWidgets.QGroupBox()
    group.setFlat(True)
    group.setStyleSheet("border:0")
    group.setLayout(vbox)    

    grid = QtWidgets.QGridLayout()
    grid.addWidget(self.canvas, 0, 0, 1, 2)
    grid.addWidget(group, 0, 3, QtCore.Qt.AlignCenter)
    grid.addWidget(self.toolbar, 1, 0)
    grid.addWidget(self.cb_jump_on_click, 2, 0)

    self.cid = self.fig.canvas.mpl_connect('button_press_event', self.on_click)

    self.setLayout(grid)
    
  def on_click(self, event):
    if event.dblclick and event.xdata:
      addr = self.calc_addr_fcn(int(event.xdata))
      if addr:
        jumpto(addr)
      else:
        warning("Unable to calculate the address")

  def format_coord_normal(self, x, y):
    try:
      if self.config.use_disk_binary or self.entropy_cfg['segm_exists']:
        addr = int(x) * self.config.entropy['step_size']
        return "Offset : 0x%08x %-30s" % (addr, "")
      else:
        addr = self.calc_addr_fcn(int(x))
        return "0x%08x - %-30s" % (addr, SegName(addr))       
    except:
      pass
    return "bad address"

  def make_normal_chart(self):
    blocks = self.data_size / self.entropy_cfg['block_size']
    self.calc_addr_fcn = self.calc_point_addr_normal

    results = list(entropy_scan(self.data, 
      self.entropy_cfg['block_size'], 
      self.entropy_cfg['step_size'])
    )
    min_value, max_value  = min(results), max(results)
    avg_values = sum(results) / len(results)

    self.fig = plt.figure(facecolor='white')
    ax = plt.subplot(111, facecolor='white')
    ax.axis([0, len(results), 0, 8])
    ax.format_coord = self.format_coord_normal
    plt.plot(results, color="#2E9AFE")    

    log("Entropy - Start address: 0x%08x" % self.config.start_addr)
    log("Entropy - End address:   0x%08x" % self.config.end_addr)
    log("Entropy - Data size: %d bytes (blocks: %d)" % (self.data_size, blocks))
    info_str = 'Entropy - Min: %.2f | Max:  %.2f | Avg: %.2f' % (min_value, max_value, avg_values)
    log(info_str)
    del self.data

    plt.xlabel('Byte range')
    plt.ylabel('Entropy')
    plt.title('Entropy levels')

    self.canvas = FigureCanvas(self.fig)
    self.toolbar = NavigationToolbar(self.canvas, self)
    self.line_edit = QtWidgets.QLineEdit()

    self.cb_jump_on_click = QtWidgets.QCheckBox("Disable double-click event")    
    self.cb_jump_on_click.stateChanged.connect(self.disable_jump_on_click)

    grid = QtWidgets.QGridLayout()
    grid.addWidget(self.canvas, 0, 0)
    grid.addWidget(self.toolbar, 1, 0)

    if not self.config.use_disk_binary or self.entropy_cfg['segm_exists']:
      grid.addWidget(self.cb_jump_on_click, 2, 0)
      self.cid = self.fig.canvas.mpl_connect('button_press_event', self.on_click)

    self.setLayout(grid)

  def disable_jump_on_click(self, state):
    if state == QtCore.Qt.Checked:
      self.fig.canvas.mpl_disconnect(self.cid)
    else:
      self.cid = self.fig.canvas.mpl_connect('button_press_event', self.on_click)


class TableItem(QtWidgets.QTableWidgetItem):  
  class ItemType:
    DEC = 0
    HEX = 1
    FLOAT = 2
    TEXT = 3

  def __init__(self, text, item_type):
    QtWidgets.QTableWidgetItem.__init__(self, text, QtWidgets.QTableWidgetItem.UserType)
    self.setFlags(QtCore.Qt.ItemIsEnabled)
    self.item_type = item_type
    self.text = text  

  def __lt__(self, other):
    if self.item_type == self.ItemType.DEC:
      return int(self.text) < int(other.text)
    elif self.item_type == self.ItemType.HEX:
      return int(self.text, 16) < int(other.text, 16)
    elif self.item_type == self.ItemType.FLOAT:
      data = float(self.text.replace("%", ""))
      other_data = float(other.text.replace("%", ""))
      return data < other_data
    elif self.item_type == self.ItemType.TEXT:
      return self.text < other.text
    else:
      raise TypeError("The ItemType specified is not supported")

class Histogram(QtWidgets.QWidget):
  def __init__(self, parent):
    QtWidgets.QWidget.__init__(self)
    self.parent   = parent
    self.config   = parent.config
    self.data     = None
    self.data_size = None
    self.get_data()
    self.make_histogram()

  def get_data(self):
    data = None

    if self.config.use_disk_binary:
      data = get_disk_binary()
    else:
      data_size = self.config.end_addr - self.config.start_addr
      data = get_loaded_bytes(self.config.start_addr, data_size, "")

    self.data = data
    self.data_size = len(data)

  def format_coord(self, x, y):    
    try:
      value = int(x)
      return "value: %d | count: %-30s" % (value, self.counter[value] )
    except:
      pass
    return "bad value"

  def make_histogram(self):    
    self.counter   = histogram(self.data)
    self.counts    = [round(100*float(byte_count)/self.data_size, 2) for byte_count in self.counter] 
    top_y          = math.ceil(max(self.counts)*10.0)/10.0
    del self.data

    self.create_table()
    fig = plt.figure(facecolor='white')
    ax = plt.subplot(111, facecolor='white')

    control_bytes    = 0
    whitespace_bytes = 0
    null_bytes       = self.counter[0]
    printable_bytes  = sum([self.counter[byte] for byte in range(0x21, 0x7F)])
    high_bytes       = sum([self.counter[byte] for byte in range(0x80, 0x100)])

    for byte in range(1, 0x21):
      if chr(byte) in string.whitespace:
        whitespace_bytes += self.counter[byte]
      else:
        control_bytes += self.counter[byte]

    log("Histogram - Data size: %d bytes" % self.data_size)
    self.log_byte_stats("NULL bytes", null_bytes)
    self.log_byte_stats("Control bytes", control_bytes)
    self.log_byte_stats("Whitespace bytes", whitespace_bytes)
    self.log_byte_stats("Printable bytes", printable_bytes)
    self.log_byte_stats("High bytes", high_bytes)

    plt.axis([0, 256, 0, top_y])
    ax.set_xlim(0, 255)
    ax.set_xticks([0, 64, 128, 192, 255])
    ax.format_coord = self.format_coord

    bar_colors = ['#2040D0','#2E9AFE']
    ax.bar(range(256), self.counts, width=1, edgecolor="black", linewidth=0.4, color=bar_colors*128)

    plt.title("Byte histogram")
    plt.xlabel('Byte range')
    plt.ylabel('Occurance [%]')

    self.canvas  = FigureCanvas(fig)
    self.toolbar = NavigationToolbar(self.canvas, self)

    grid = QtWidgets.QGridLayout()
    grid.addWidget(self.canvas, 0, 0)
    grid.addWidget(self.toolbar, 1, 0)
    grid.addWidget(self.table, 0, 1, 0, 2)
    self.setLayout(grid)

  def log_byte_stats(self, name, n_bytes):
    log("Histogram - %-18s: %6d (%2.02f %%)" % (name, n_bytes, (float(n_bytes)/self.data_size*100)))

  def create_table(self):
    self.table = QtWidgets.QTableWidget()
    self.table.setColumnCount(5)
    self.table.setColumnWidth(0, 5)
    self.table.setColumnWidth(1, 5)
    self.table.setColumnWidth(2, 1)
    self.table.setColumnWidth(3, 5)
    self.table.setColumnWidth(4, 5)
    self.table.setHorizontalHeaderLabels(["Dec", "Hex", "Char", "Count", "Percent"])
    self.table.verticalHeader().hide()
    self.table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    
    printable = map(ord, string.printable[:-6])
    for byte, count in enumerate(self.counter):
      char = 0      
      if byte in printable:
        char = byte

      dec_item     = TableItem("%d" % byte,  TableItem.ItemType.DEC)
      hex_item     = TableItem("%02X" % byte,TableItem.ItemType.HEX)
      char_item    = TableItem("%c" % char,  TableItem.ItemType.TEXT)
      count_item   = TableItem("%d" % count, TableItem.ItemType.DEC)
      percert_item = TableItem("%s%%" % self.counts[byte], TableItem.ItemType.FLOAT)

      self.table.insertRow(byte)
      self.table.setItem(byte, 0, dec_item)
      self.table.setItem(byte, 1, hex_item)
      self.table.setItem(byte, 2, char_item)
      self.table.setItem(byte, 3, count_item)
      self.table.setItem(byte, 4, percert_item)

    self.table.horizontalHeader().setStretchLastSection(1);
    self.table.resizeRowsToContents()
    self.table.resizeColumnsToContents()
    self.table.setSortingEnabled(True)
    self.table.sortItems(3, QtCore.Qt.DescendingOrder)
    self.table.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows)  

def make_ida6_compatible():
  """ Compatibility between IDA 6.X and 7.X """

  global get_bytes
  global put_bytes

  if IDA_SDK_VERSION < 700:
    get_bytes = get_loaded_bytes
    put_bytes = my_put_bytes

class IDAtropyForm(PluginForm):
  def __init__(self):
    super(IDAtropyForm, self).__init__()
    self.config = Config()

    # disable timeout for scripts
    self.old_timeout = idaapi.set_script_timeout(0)

  def OnCreate(self, form):
    if USE_PYQT5:
      self.parent = self.FormToPyQtWidget(form)
    else:
      self.parent = self.FormToPySideWidget(form)
    self.PopulateForm()

  def RemoveTab(self, index):
    pass

  def PopulateForm(self):
    self.tabs = QtWidgets.QTabWidget()
    self.tabs.setMovable(True)
    self.tabs.setTabsClosable(True)
    self.tabs.tabCloseRequested.connect(self.remove_tabs)
    self.tabs.addTab(Options(self), "Options")

    layout = QtWidgets.QVBoxLayout()
    layout.addWidget(self.tabs)
    self.parent.setLayout(layout)

  def remove_tabs(self, index):
    if not isinstance(self.tabs.widget(index), Options):
      self.tabs.removeTab(index)

  def OnClose(self, form):
    idaapi.set_script_timeout(self.old_timeout)
    print "[%s] Form closed." % PLUG_NAME

class IDAtropy_t(plugin_t):
    flags = PLUGIN_UNL
    comment = "IDAtropy"
    help = ""
    wanted_name = PLUG_NAME
    wanted_hotkey = "Alt-F10"

    def init(self):
      self.icon_id = 0
      make_ida6_compatible()
      return PLUGIN_OK

    def run(self, arg=0):
      
      if not 'ERROR_MATPLOTLIB' in globals():
        f = IDAtropyForm()
        f.Show(PLUG_NAME)
      else:
        warning("%s - The plugin requires matplotlib" % PLUG_NAME)

    def term(self):
        pass

def PLUGIN_ENTRY():    
    return IDAtropy_t()

if __name__ == '__main__':
  log("Plugin loaded")