class PixelWidget(QWidget): def __init__(self, form, bufhandler): super(PixelWidget, self).__init__() self.form = form self.set_zoom(10) self.maxPixelsPerLine = 64 self.maxPixelsTotal = 0 self.prev_mouse_y = 0 self.key = None self.buffers = None self.offs = 0 self.base = 0 self.fm = None self.filter_idx = 0 self.mouseOffs = 0 self.sync = True self.bh = bufhandler self.mouse_abs_x = 0 self.mouse_abs_y = 0 self.elemX = 0 self.elemY = 0 self.rect_x = 0 self.rect_x_width = 0 self.lock_width = False self.lock_sync = False self.link_pixel = True self.highlight_cursor = False self.render_data = True self.cur_formatter_idx = 0 self.max_formatters = 2 self.setMouseTracking(True) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.sh = SignalHandler() self.statechanged = self.sh.pw_statechanged self.next_filter = self.sh.pw_next_filter self.prev_filter = self.sh.pw_prev_filter self.qp = QPainter() self.show() def paintEvent(self, event): if not self.fm: return # set leftmost x-coordinate of graph zoom_level = self.get_zoom() self.rect_x_width = self.get_width() * zoom_level self.rect_x = (self.rect().width() / 2) - (self.rect_x_width / 2) self.qp.begin(self) # what is a good default font for OSX/Linux? self.qp.setFont(QFont(FONT_DEFAULT)) # fill background self.qp.fillRect(self.rect(), Qt.black) content_addr = content_size = None if self.fm.support_selection: selected, start, end = ida_kernwin.read_range_selection(None) if selected: content_addr = start #min(start, end) content_size = end - start #max(start, end) - content_addr # use colorfilter to render image img = self.render_image(addr=content_addr, buf_size=content_size) if img: """ if zoom_level > 6: opacity = self.qp.opacity() full_opacity_zoom = 40.0 cur_opacity = (1.0 - (full_opacity_zoom - float(min(zoom_level-1, full_opacity_zoom)))/full_opacity_zoom) self.qp.setOpacity(1.0-cur_opacity) """ # draw image self.qp.drawImage( QRect( QPoint(self.rect_x, 0), QPoint(self.rect_x + self.get_width() * zoom_level, (self.get_pixels_total() / self.get_width()) * zoom_level)), img) # TODO: pen color contrast # TODO: data export: render data # TODO: default fonts / OS? # TODO: optimization # FIXME: there's a bug with gaps/unmapped buffers if self.render_data and not self.fm.disable_data and zoom_level > 6: self.qp.setPen(QColor(Qt.white)) fontsize = self.qp.font().pointSize() font = self.qp.font() font.setPointSize(zoom_level / 3) self.qp.setFont(font) opacity = self.qp.opacity() full_opacity_zoom = 28 cur_opacity = ( 1.0 - (full_opacity_zoom - float(min(zoom_level - 1, full_opacity_zoom))) / full_opacity_zoom) self.qp.setOpacity(cur_opacity) m = self.qp.fontMetrics() x = y = 0 num_pixels_per_row = self.get_width() if self.cur_formatter_idx == 0: sample = "%c" % (ord('X')) cwidth = m.width(sample) cheight = m.height() for mapped, buf in self.buffers: for i in range(len(buf)): if mapped: b = ord(buf[i]) data = "%c" % (chr(b) if b in range( 0x20, 0x7E) else ".") self.qp.drawStaticText( self.rect_x + x * zoom_level + (zoom_level - cwidth) / 2, y * zoom_level + (zoom_level - cheight) / 2, QStaticText(data)) x = (i + 1) % num_pixels_per_row if not x: y = y + 1 elif self.cur_formatter_idx == 1: sample = "%02X" % (ord('X')) cwidth = m.width(sample) cheight = m.height() for mapped, buf in self.buffers: for i in range(len(buf)): if mapped: data = "%02X" % ord(buf[i]) self.qp.drawStaticText( self.rect_x + x * zoom_level + (zoom_level - cwidth) / 2, y * zoom_level + (zoom_level - cheight) / 2, QStaticText(data)) x = (i + 1) % num_pixels_per_row if not x: y = y + 1 self.qp.setOpacity(opacity) font.setPointSize(fontsize) self.qp.setFont(font) if self.show_address_range and self.fm.link_pixel: self.render_slider(addr=content_addr, buf_size=content_size) # get and draw annotations and pointers annotations = self.fm.on_get_annotations( content_addr if content_addr else self.get_address(), self.get_pixels_total(), self.mouseOffs) if annotations: self.render_annotations(annotations) self.qp.end() return def render_slider(self, addr=None, buf_size=None): if addr is None or buf_size is None: addr = self.base + self.offs buf_size = self.get_pixels_total() lowest_ea = ida_idaapi.get_inf_structure().get_minEA() highest_ea = ida_idaapi.get_inf_structure().get_maxEA() start_offs = addr - lowest_ea addr_space = highest_ea - lowest_ea perc_s = float(start_offs) / float(addr_space) perc_e = float(start_offs + buf_size) / float(addr_space) bar_width = 20 spaces_bar = 5 bar_x = self.rect_x - spaces_bar - bar_width bar_y = 5 bar_height = self.rect().height() - 2 * bar_y self.qp.fillRect(bar_x, bar_y, bar_width, bar_height, QColor(0x191919)) slider_offs_s = int(round(perc_s * bar_height)) slider_offs_e = int(round(perc_e * bar_height)) spaces_slider = 1 slider_x = bar_x + spaces_slider slider_y = bar_y + slider_offs_s slider_width = bar_width - 2 * spaces_slider # limit slider height to bar_height slider_height = max( min(slider_offs_e - slider_offs_s, bar_height - (slider_y - bar_y)), 4) self.qp.fillRect(slider_x, slider_y, slider_width, slider_height, QColor(0x404040)) # draw addresses top = '%X:' % self.get_address() bottom = '%X' % (self.get_address() + ((self.get_pixels_total() / self.get_width()) - 1) * self.get_width()) self.qp.setPen(QColor(0x808080)) self.qp.drawText( self.rect_x - self.qp.fontMetrics().width(top) - bar_width - 2 * spaces_bar, self.qp.fontMetrics().height(), top) self.qp.drawText( self.rect_x - self.qp.fontMetrics().width(bottom) - bar_width - 2 * spaces_bar, self.rect().height() - self.qp.fontMetrics().height() / 2, bottom) return def render_image(self, addr=None, buf_size=None, cursor=True): size = self.size() self.maxPixelsTotal = self.get_width() * (size.height() / self.pixelSize) if addr is None or buf_size is None: addr = self.base + self.offs buf_size = self.get_pixels_total() self.buffers = self.bh.get_buffers(addr, buf_size) img = QImage(self.get_width(), size.height() / self.pixelSize, QImage.Format_RGB32) pixels = self.fm.on_process_buffer(self.buffers, addr, self.get_pixels_total(), self.mouseOffs) x = y = 0 # transparency effect for unmapped bytes transparency_dark = [qRgb(0x2F, 0x4F, 0x4F), qRgb(0x00, 0x00, 0x00)] transparency_err = [qRgb(0x7F, 0x00, 0x00), qRgb(0x33, 0x00, 0x00)] for mapped, pix in pixels: if not mapped: if pix is None: pix = transparency_dark[(x & 2 != 0) ^ (y & 2 != 0)] img.setPixel(x, y, pix) x = (x + 1) % self.get_width() if not x: y = y + 1 if len(pixels) != self.get_pixels_total(): for i in xrange(self.get_pixels_total() - len(pixels)): pix = transparency_err[(x & 2 != 0) ^ (y & 2 != 0)] img.setPixel(x, y, pix) x = (x + 1) % self.get_width() if not x: y = y + 1 if ((cursor and self.fm.highlight_cursor) and self.mouse_abs_x >= self.rect_x and self.mouse_abs_x < self.rect_x + self.rect_x_width): coords = self.get_coords_by_address(self.get_cursor_address()) if coords: x, y = coords else: x = self.get_elem_x() y = self.get_elem_y() p = QPoint(x, y) img.setPixel(p, ~(img.pixelColor(p)).rgb()) return img def render_annotations(self, annotations=[]): a_offs = 20 base_x = self.rect_x + self.get_width() * self.pixelSize + a_offs + 10 base_y = self.qp.fontMetrics().height() offs_x = 5 offs_y = base_y for coords, arr_color, ann, txt_color in annotations: # draw arrow (experimental / WIP) self.qp.setPen( QColor(Qt.white if txt_color is None else txt_color)) self.qp.drawText(base_x + 10, (base_y + offs_y) / 2, ann) target_x = target_y = None if coords: if isinstance(coords, tuple): target_x, target_y = coords else: ptr = self.get_coords_by_address(coords) if ptr: target_x, target_y = ptr if target_x is not None and target_y is not None: target_x *= self.get_zoom() target_y *= self.get_zoom() self.qp.setPen( QColor(Qt.white if arr_color is None else arr_color)) path = QPainterPath() path.moveTo(base_x + offs_x, (base_y + offs_y) / 2 - base_y / 2) path.lineTo(base_x + offs_x - 4 - a_offs, (base_y + offs_y) / 2 - base_y / 2) # left path.lineTo(base_x + offs_x - 4 - a_offs, ((target_y / 10) * 9) + self.get_zoom() / 2) # down path.lineTo(self.rect_x + target_x + self.get_zoom() / 2, ((target_y / 10) * 9) + self.get_zoom() / 2) # left path.lineTo(self.rect_x + target_x + self.get_zoom() / 2, target_y + self.get_zoom() / 2) # down a_offs = max(a_offs - 2, 0) self.qp.drawPath(path) else: if not isinstance(coords, tuple): direction = self.get_target_direction(coords) if direction: self.qp.setPen( QColor(Qt.white if arr_color is None else arr_color)) m = self.qp.fontMetrics() dirhint = ['', '<<', '>>'][direction] cwidth = m.width("%s" % (dirhint)) self.qp.drawText(base_x - cwidth, (base_y + offs_y) / 2, dirhint) offs_y += 2 * base_y + 5 return # functions that can be called by filters # must no be called from within on_process_buffer() def on_filter_request_update(self, ea=None, center=True): if not ea: self.repaint() else: curea = self.get_address() if ea < curea or ea >= curea + self.get_pixels_total(): # TODO: verify that ea is valid after following operation if center: ea -= self.get_pixels_total() / 2 self.set_addr(ea) else: self.repaint() def on_filter_update_zoom(self, zoom): self.set_zoom(zoom) return def on_filter_update_zoom_delta(self, delta): self.set_zoom_delta(delta) return # end of functions that can be called by filters def show_help(self): ida_kernwin.info("%s" % PLUGIN_HELP) def keyPressEvent(self, event): if self.key is None: self.key = event.key() return def keyReleaseEvent(self, event): update = False key = event.key() modifiers = event.modifiers() shift_pressed = ((modifiers & Qt.ShiftModifier) == Qt.ShiftModifier) ctrl_pressed = ((modifiers & Qt.ControlModifier) == Qt.ControlModifier) if key == Qt.Key_F1 and ctrl_pressed: self.show_help() elif key == Qt.Key_G: addr = ask_addr(self.base + self.offs, 'Jump to address') if addr is not None: if self.sync: ida_kernwin.jumpto(addr) else: minea = ida_idaapi.get_inf_structure().get_minEA() maxea = ida_idaapi.get_inf_structure().get_maxEA() dst = min(max(addr, minea), maxea) self.set_addr(dst) elif key == Qt.Key_S: if not self.fm.lock_sync: self.set_sync_state(not self.get_sync_state()) update = True elif key == Qt.Key_T: self.render_data = not self.render_data self.repaint() elif key == Qt.Key_D: self.cur_formatter_idx = (self.cur_formatter_idx + 1) % self.max_formatters self.repaint() elif key == Qt.Key_N: self.next_filter.emit() elif key == Qt.Key_B: self.prev_filter.emit() elif key == Qt.Key_F2: hlp = self.fm.help if hlp is None: hlp = 'Help unavailable' ida_kernwin.info('%s\n\n' % hlp) elif key == Qt.Key_F12: img = self.render_image(cursor=False) img = img.scaled(img.width() * self.pixelSize, img.height() * self.pixelSize, Qt.KeepAspectRatio, Qt.FastTransformation) done = False i = 0 while not done: fname = 'IDACyber_%04d.bmp' % i if not os.path.isfile(fname): if img.save(fname): ida_kernwin.msg('File exported to %s\n' % fname) else: ida_kernwin.warning( 'Error exporting screenshot to %s.' % fname) done = True i += 1 if i > 40: ida_kernwin.warning('Aborted. Error exporting screenshot.') break elif key == Qt.Key_PageDown: self.set_offset_delta(-self.get_pixels_total()) update = True elif key == Qt.Key_PageUp: self.set_offset_delta(self.get_pixels_total()) update = True elif key == Qt.Key_Down: if shift_pressed: self.set_offset_delta(-1) else: self.set_offset_delta(-self.get_width()) update = True elif key == Qt.Key_Up: if shift_pressed: self.set_offset_delta(1) else: self.set_offset_delta(self.get_width()) update = True elif key == Qt.Key_Plus: if ctrl_pressed: self.set_zoom_delta(1) update = True elif key == Qt.Key_Minus: if ctrl_pressed: self.set_zoom_delta(-1) update = True self.key = None if update: if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() self.statechanged.emit() self.repaint() return def mouseReleaseEvent(self, event): self.prev_mouse_y = event.pos().y() self.fm.on_mb_click(event, self.get_address(), self.get_pixels_total(), self.mouseOffs) if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() self.statechanged.emit() return def mouseDoubleClickEvent(self, event): if self.link_pixel and event.button() == Qt.LeftButton: addr = self.base + self.offs + self._get_offs_by_pos(event.pos()) ida_kernwin.jumpto(addr) return def wheelEvent(self, event): delta = event.angleDelta().y() / 120 # zoom if self.key == Qt.Key_Control: self.set_zoom_delta(delta) # width elif self.key == Qt.Key_X: if not self.lock_width: self.set_width_delta(delta) # offset (fine) elif self.key == Qt.Key_Shift: self.set_offset_delta(delta) if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() elif self.key == Qt.Key_H: if not self.lock_width: less = delta < 0 w = -8 if less else 8 self.set_width((self.get_width() & 0xFFFFFFF8) + w) # offset (coarse) else: self.set_offset_delta(delta * self.get_width()) if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() self.statechanged.emit() self.repaint() return def mouseMoveEvent(self, event): x = event.pos().x() y = event.pos().y() within_graph = (x >= self.rect_x and x < self.rect_x + self.rect_x_width) if within_graph: if event.buttons() == Qt.NoButton: self._update_mouse_coords(event.pos()) self.mouseOffs = self._get_offs_by_pos(event.pos()) if self.link_pixel and self.highlight_cursor: highlight_item( ida_bytes.get_item_head(self.get_cursor_address())) elif self.highlight_cursor: unhighlight_item() self.setToolTip( self.fm.on_get_tooltip(self.get_address(), self.get_pixels_total(), self.mouseOffs)) # zoom elif self.key == Qt.Key_Control: self.set_zoom_delta(-1 if y > self.prev_mouse_y else 1) # width elif self.key == Qt.Key_X: if not self.lock_width: self.set_width_delta(-1 if y > self.prev_mouse_y else 1) elif self.key == Qt.Key_H: if not self.lock_width: less = y > self.prev_mouse_y delta = -16 if less else 16 self.set_width((self.get_width() & 0xFFFFFFF0) + delta) # scrolling (offset) elif y != self.prev_mouse_y: # offset (fine) delta = y - self.prev_mouse_y # offset (coarse) if self.key != Qt.Key_Shift: delta *= self.get_width() self.set_offset_delta(delta) self.prev_mouse_y = y self.x = x self.statechanged.emit() self.repaint() return def set_sync_state(self, sync): self.sync = sync def get_sync_state(self): return self.sync def get_filter_idx(self): return self.filter_idx def set_filter(self, fltobj, idx): if self.fm: self.fm.on_deactivate() if fltobj: self.fm = fltobj """load filter config""" self.set_sync_state(self.fm.sync) self.lock_width = self.fm.lock_width self.set_width(self.fm.width) self.lock_sync = self.fm.lock_sync self.show_address_range = self.fm.show_address_range # disabled for now # self.set_zoom(self.fm.zoom) self.link_pixel = self.fm.link_pixel self.highlight_cursor = self.fm.highlight_cursor self.statechanged.emit() """load filter config end""" self.fm.on_activate(idx) self.filter_idx = idx unhighlight_item() self.repaint() def set_addr(self, ea, new_cursor=None): base = self.bh.get_base(ea) self._set_base(base) self._set_offs(ea - base) if new_cursor: self.set_cursor_address(new_cursor) if self.highlight_cursor: highlight_item(ea) self.repaint() def get_zoom(self): return self.pixelSize def set_zoom(self, zoom): self.pixelSize = zoom def set_zoom_delta(self, dzoom): self.set_zoom(max(1, self.pixelSize + dzoom)) return def get_width(self): return self.maxPixelsPerLine def get_pixels_total(self): return self.maxPixelsTotal def get_address(self): return self.base + self.offs def get_cursor_address(self): return self.get_address() + self.mouseOffs def set_cursor_address(self, ea): self.mouseOffs = ea - self.get_address() def get_coords_by_address(self, address): base = self.get_address() # if address is visible in current window if address >= base and address < base + self.get_pixels_total(): offs = address - base x = offs % self.get_width() y = offs / (self.get_width()) return (x, y) return None def get_target_direction(self, address): base = self.get_address() # if address is visible in current window direction = None if address >= base and address < base + self.get_pixels_total(): direction = 0 elif address < base: direction = 1 else: direction = 2 return direction def set_width(self, width): self.maxPixelsPerLine = max(1, width) def set_width_delta(self, dwidth): self.maxPixelsPerLine = max(1, self.maxPixelsPerLine + dwidth) def set_offset_delta(self, doffs): newea = self.base + self.offs - doffs minea = ida_idaapi.get_inf_structure().get_minEA() maxea = ida_idaapi.get_inf_structure().get_maxEA() if doffs < 0: delta = doffs if newea < maxea else doffs - (maxea - newea) else: delta = doffs if newea >= minea else doffs - (minea - newea) self._set_offs(self.offs - delta) def _get_offs_by_pos(self, pos): elemX = self.get_elem_x() elemY = self.get_elem_y() offs = elemY * self.get_width() + elemX return offs def _update_mouse_coords(self, pos): x = pos.x() y = pos.y() self.mouse_abs_x = x self.mouse_abs_y = y self.elemX = max( 0, min((max(0, x - self.rect_x)) / self.pixelSize, self.get_width() - 1)) self.elemY = min(y / self.pixelSize, self.maxPixelsTotal / self.get_width() - 1) def get_elem_x(self): return self.elemX def get_elem_y(self): return self.elemY def _set_offs(self, offs): self.offs = offs def _set_base(self, ea): self.base = ea
class PixelWidget(QWidget): def __init__(self, form, bufhandler): super(PixelWidget, self).__init__() self.form = form self.set_zoom(10) self.is_dragging_graph = False self.maxPixelsPerLine = 64 self.maxPixelsTotal = 0 self.prev_mouse_y = 0 self.key = None self.buffers = None self.offs = 0 self.base = 0 self.fm = None self.filter_idx = 0 self.mouseOffs = 0 self.sync = True self.bh = bufhandler self.mouse_abs_x = 0 self.mouse_abs_y = 0 self.elemX = 0 self.elemY = 0 self.rect_x = 0 self.rect_x_width = 0 self.lock_width = False self.lock_sync = False self.link_pixel = True self.highlight_cursor = False self.textbox_content = None self.textbox_content_type = 0 self.cur_formatter_idx = 2 self.formatters = [(0, "off"), (1, "ascii"), (2, "hex")] self.max_formatters = len(self.formatters) # composition modes: https://doc.qt.io/qt-5/qpainter.html#CompositionMode-enum """ self.composition_modes = [ (QPainter.CompositionMode_SourceOver, "QPainter.CompositionMode_SourceOver"), (QPainter.CompositionMode_DestinationOver, "QPainter.CompositionMode_DestinationOver"), (QPainter.CompositionMode_Clear, "QPainter.CompositionMode_Clear"), (QPainter.CompositionMode_Source, "QPainter.CompositionMode_Source"), (QPainter.CompositionMode_Destination, "QPainter.CompositionMode_Destination"), (QPainter.CompositionMode_SourceIn, "QPainter.CompositionMode_SourceIn"), (QPainter.CompositionMode_DestinationIn, "QPainter.CompositionMode_DestinationIn"), (QPainter.CompositionMode_SourceOut, "QPainter.CompositionMode_SourceOut"), (QPainter.CompositionMode_DestinationOut, "QPainter.CompositionMode_DestinationOut"), (QPainter.CompositionMode_SourceAtop, "QPainter.CompositionMode_SourceAtop"), (QPainter.CompositionMode_DestinationAtop, "QPainter.CompositionMode_DestinationAtop"), (QPainter.CompositionMode_Xor, "QPainter.CompositionMode_Xor"), (QPainter.CompositionMode_Plus, "QPainter.CompositionMode_Plus"), (QPainter.CompositionMode_Multiply, "QPainter.CompositionMode_Multiply"), (QPainter.CompositionMode_Screen, "QPainter.CompositionMode_Screen"), (QPainter.CompositionMode_Overlay, "QPainter.CompositionMode_Overlay"), (QPainter.CompositionMode_Darken, "QPainter.CompositionMode_Darken"), (QPainter.CompositionMode_Lighten, "QPainter.CompositionMode_Lighten"), (QPainter.CompositionMode_ColorDodge, "QPainter.CompositionMode_ColorDodge"), (QPainter.CompositionMode_ColorBurn, "QPainter.CompositionMode_ColorBurn"), (QPainter.CompositionMode_HardLight, "QPainter.CompositionMode_HardLight"), (QPainter.CompositionMode_SoftLight, "QPainter.CompositionMode_SoftLight"), (QPainter.CompositionMode_Difference, "QPainter.CompositionMode_Difference"), (QPainter.CompositionMode_Exclusion, "QPainter.CompositionMode_Exclusion"), (QPainter.RasterOp_SourceOrDestination, "QPainter.RasterOp_SourceOrDestination"), (QPainter.RasterOp_SourceAndDestination, "QPainter.RasterOp_SourceAndDestination"), (QPainter.RasterOp_SourceXorDestination, "QPainter.RasterOp_SourceXorDestination"), (QPainter.RasterOp_NotSourceAndNotDestination, "QPainter.RasterOp_NotSourceAndNotDestination"), (QPainter.RasterOp_NotSourceOrNotDestination, "QPainter.RasterOp_NotSourceOrNotDestination"), (QPainter.RasterOp_NotSourceXorDestination, "QPainter.RasterOp_NotSourceXorDestination"), (QPainter.RasterOp_NotSource, "QPainter.RasterOp_NotSource"), (QPainter.RasterOp_NotSourceAndDestination, "QPainter.RasterOp_NotSourceAndDestination"), (QPainter.RasterOp_SourceAndNotDestination, "QPainter.RasterOp_SourceAndNotDestination"), (QPainter.RasterOp_NotSourceOrDestination, "QPainter.RasterOp_NotSourceOrDestination"), (QPainter.RasterOp_ClearDestination, "QPainter.RasterOp_ClearDestination"), (QPainter.RasterOp_SetDestination, "QPainter.RasterOp_SetDestination"), (QPainter.RasterOp_NotDestination, "QPainter.RasterOp_NotDestination"), (QPainter.RasterOp_SourceOrNotDestination, "QPainter.RasterOp_SourceOrNotDestination")] """ self.composition_modes = [ (QPainter.CompositionMode_Overlay, "Comp_Overlay"), (QPainter.CompositionMode_SourceOver, "Comp_SourceOver"), (QPainter.CompositionMode_Xor, "Comp_Xor"), (QPainter.CompositionMode_SoftLight, "Comp_SoftLight"), (QPainter.CompositionMode_Difference, "Comp_Difference"), (QPainter.CompositionMode_Exclusion, "Comp_Exclusion"), (QPainter.RasterOp_NotSourceAndNotDestination, "Raster_NotSourceAndNotDestination"), (QPainter.RasterOp_SourceAndNotDestination, "Raster_SourceAndNotDestination"), (QPainter.RasterOp_ClearDestination, "Raster_ClearDestination") ] self.cur_compos_mode = 0 self.setMouseTracking(True) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.sh = SignalHandler() self.statechanged = self.sh.pw_statechanged self.next_filter = self.sh.pw_next_filter self.prev_filter = self.sh.pw_prev_filter self.qp = QPainter() self.show() def paintEvent(self, event): if not self.fm: return # set leftmost x-coordinate of graph zoom_level = self.get_zoom() self.rect_x_width = self.get_pixel_qty_per_line() * zoom_level self.rect_x = (self.rect().width() / 2) - (self.rect_x_width / 2) self.qp.begin(self) # what is a good default font for OSX/Linux? self.qp.setFont(QFont(FONT_DEFAULT)) # fill background self.qp.fillRect(self.rect(), Qt.black) content_addr = content_size = None if self.fm.support_selection: selected, start, end = ida_kernwin.read_range_selection(None) if selected: content_addr = start #min(start, end) content_size = end - start #max(start, end) - content_addr # use colorfilter to render image img = self.paint_image(addr=content_addr, buf_size=content_size) if img: """ if zoom_level > 6: opacity = self.qp.opacity() full_opacity_zoom = 40.0 cur_opacity = (1.0 - (full_opacity_zoom - float(min(zoom_level-1, full_opacity_zoom)))/full_opacity_zoom) self.qp.setOpacity(1.0-cur_opacity) """ # draw image self.qp.drawImage( QRect( QPoint(self.rect_x, 0), QPoint( self.rect_x + self.get_pixel_qty_per_line() * zoom_level, (self.get_pixel_qty() / self.get_pixel_qty_per_line()) * zoom_level)), img) # TODO: pen color contrast # TODO: data export: render data # TODO: default fonts / OS? # TODO: optimization # FIXME: there's a bug with gaps/unmapped buffers if (self.cur_formatter_idx and not self.fm.disable_data and zoom_level >= 10 and self.get_pixel_qty() < 70 * 70): self.qp.setPen(QColor(Qt.white)) fontsize = self.qp.font().pointSize() font = self.qp.font() font.setPointSize(zoom_level / 3) #font.setPixelSize(zoom_level) self.qp.setFont(font) opacity = self.qp.opacity() full_opacity_zoom = 28 cur_opacity = ( 1.0 - (full_opacity_zoom - float(min(zoom_level - 1, full_opacity_zoom))) / full_opacity_zoom) self.qp.setOpacity(cur_opacity) m = self.qp.fontMetrics() x = y = 0 num_pixels_per_line = self.get_pixel_qty_per_line() cm = self.qp.compositionMode() self.qp.setCompositionMode( self.composition_modes[self.cur_compos_mode][0]) if self.formatters[self.cur_formatter_idx][0] == 1: fmt = lambda c: "%c" % c if c in range(0x20, 0x7e) else "." elif self.formatters[self.cur_formatter_idx][0] == 2: fmt = lambda c: "%02X" % c for mapped, buf in self.buffers: for i in range(len(buf)): if mapped: b = buf[i] data = fmt(b) self.qp.drawText(self.rect_x + x * zoom_level, y * zoom_level, zoom_level, zoom_level, Qt.AlignCenter, data) x = (i + 1) % num_pixels_per_line if not x: y = y + 1 # restore attributes self.qp.setCompositionMode(cm) self.qp.setOpacity(opacity) font.setPointSize(fontsize) self.qp.setFont(font) if self.show_address_range and self.fm.link_pixel: self.paint_slider(addr=content_addr, buf_size=content_size) # get and draw annotations and pointers annotations = self.fm.on_get_annotations( content_addr if content_addr else self.get_address(), self.get_pixel_qty(), self.mouseOffs) if annotations: self.paint_annotations(annotations) self.paint_status() if self.textbox_content: self.paint_text_box() self.qp.end() return def paint_image(self, addr=None, buf_size=None, cursor=True): size = self.size() self.set_pixel_qty(self.get_pixel_qty_per_line() * int(size.height() / self.pixelSize)) if addr is None or buf_size is None: addr = self.base + self.offs buf_size = self.get_pixel_qty() self.buffers = self.bh.get_buffers(addr, buf_size) img = QImage(self.get_pixel_qty_per_line(), size.height() / self.pixelSize, QImage.Format_RGB32) pixels = self.fm.on_process_buffer(self.buffers, addr, self.get_pixel_qty(), self.mouseOffs) x = y = 0 # transparency effect for unmapped bytes transparency_dark = [qRgb(0x2F, 0x4F, 0x4F), qRgb(0x00, 0x00, 0x00)] transparency_err = [qRgb(0x7F, 0x00, 0x00), qRgb(0x33, 0x00, 0x00)] for mapped, pix in pixels: if not mapped: if pix is None: pix = transparency_dark[(x & 2 != 0) ^ (y & 2 != 0)] img.setPixel(x, y, pix) x = (x + 1) % self.get_pixel_qty_per_line() if not x: y = y + 1 if len(pixels) != self.get_pixel_qty(): for i in range(self.get_pixel_qty() - len(pixels)): pix = transparency_err[(x & 2 != 0) ^ (y & 2 != 0)] img.setPixel(x, y, pix) x = (x + 1) % self.get_pixel_qty_per_line() if not x: y = y + 1 if ((cursor and self.fm.highlight_cursor) and self.mouse_abs_x >= self.rect_x and self.mouse_abs_x < self.rect_x + self.rect_x_width): coords = self.get_coords_by_address(self.get_cursor_address()) if coords: x, y = coords else: x = self.get_elem_x() y = self.get_elem_y() p = QPoint(x, y) img.setPixel(p, (~(img.pixelColor(p)).rgb() & 0xFFFFFFFF)) return img def paint_annotations(self, annotations=[]): a_offs = 20 base_x = self.rect_x + self.get_pixel_qty_per_line( ) * self.pixelSize + a_offs + 10 base_y = self.qp.fontMetrics().height() offs_x = 5 offs_y = base_y for coords, arr_color, ann, txt_color in annotations: # draw arrow (experimental / WIP) self.qp.setPen( QColor(Qt.white if txt_color is None else txt_color)) self.qp.drawText(base_x + 10, (base_y + offs_y) / 2, ann) target_x = target_y = None if coords: if isinstance(coords, tuple): target_x, target_y = coords else: ptr = self.get_coords_by_address(coords) if ptr: target_x, target_y = ptr if target_x is not None and target_y is not None: target_x *= self.get_zoom() target_y *= self.get_zoom() self.qp.setPen( QColor(Qt.white if arr_color is None else arr_color)) path = QPainterPath() path.moveTo(base_x + offs_x, (base_y + offs_y) / 2 - base_y / 2) path.lineTo(base_x + offs_x - 4 - a_offs, (base_y + offs_y) / 2 - base_y / 2) # left path.lineTo(base_x + offs_x - 4 - a_offs, ((target_y / 10) * 9) + self.get_zoom() / 2) # down path.lineTo(self.rect_x + target_x + self.get_zoom() / 2, ((target_y / 10) * 9) + self.get_zoom() / 2) # left path.lineTo(self.rect_x + target_x + self.get_zoom() / 2, target_y + self.get_zoom() / 2) # down a_offs = max(a_offs - 2, 0) self.qp.drawPath(path) else: if not isinstance(coords, tuple): direction = self.get_target_direction(coords) if direction: self.qp.setPen( QColor(Qt.white if arr_color is None else arr_color)) m = self.qp.fontMetrics() dirhint = ['', '<<', '>>'][direction] cwidth = m.width("%s" % (dirhint)) self.qp.drawText(base_x - cwidth, (base_y + offs_y) / 2, dirhint) offs_y += 2 * base_y + 5 return def paint_slider(self, addr=None, buf_size=None): if addr is None or buf_size is None: addr = self.base + self.offs buf_size = self.get_pixel_qty() lowest_ea = ida_idaapi.get_inf_structure().get_minEA() highest_ea = ida_idaapi.get_inf_structure().get_maxEA() start_offs = addr - lowest_ea addr_space = highest_ea - lowest_ea perc_s = float(start_offs) / float(addr_space) perc_e = float(start_offs + buf_size) / float(addr_space) bar_width = 20 spaces_bar = 5 bar_x = self.rect_x - spaces_bar - bar_width bar_y = 5 bar_height = self.rect().height() - 2 * bar_y self.qp.fillRect(bar_x, bar_y, bar_width, bar_height, QColor(0x191919)) slider_offs_s = perc_s * bar_height slider_offs_e = perc_e * bar_height spaces_slider = 1 slider_x = bar_x + spaces_slider slider_y = bar_y + slider_offs_s slider_width = bar_width - 2 * spaces_slider # limit slider height to bar_height slider_height = max( min(slider_offs_e - slider_offs_s, bar_height - (slider_y - bar_y)), 4) self.qp.fillRect(slider_x, slider_y, slider_width, slider_height, QColor(0x404040)) #self.slider_coords = ((slider_x, slider_y), (slider_x+slider_width, slider_y+slider_height)) self.qp.setPen(QColor(0x808080)) # draw addresses addr_low = '%X:' % self.get_address() addr_hi = '%X' % int(self.get_address() + ( (self.get_pixel_qty() / self.get_pixel_qty_per_line()) - 1) * self.get_pixel_qty_per_line()) self.qp.drawText( self.rect_x - self.qp.fontMetrics().width(addr_low) - bar_width - 2 * spaces_bar, self.qp.fontMetrics().height(), addr_low) self.qp.drawText( self.rect_x - self.qp.fontMetrics().width(addr_hi) - bar_width - 2 * spaces_bar, self.rect().height() - self.qp.fontMetrics().height() / 2, addr_hi) return def display_help_box(self, text, isFilter=False): if text == self.textbox_content or text is None or not (len(text)): self.textbox_content = None return self.textbox_content_type = 0 if isFilter else 1 self.textbox_content = text return def paint_text_box(self, borderSize=6): bar_width = 20 spaces_bar = 5 base_x = self.rect().width() / 2 if self.textbox_content_type == 0: lines = self.get_filter_helptext().splitlines() else: lines = self.textbox_content.splitlines() line_width = 0 for line in lines: line_width = max(line_width, self.qp.fontMetrics().width(line)) text_x_pos = base_x - line_width / 2 cm = self.qp.compositionMode() self.qp.setCompositionMode(QPainter.CompositionMode_HardLight) total_text_height = len(lines) * self.qp.fontMetrics().height() self.qp.fillRect( text_x_pos - borderSize, self.rect().height() / 2 - total_text_height / 2 - borderSize, line_width + borderSize * 2, total_text_height + borderSize, QColor(0x202020)) self.qp.setPen(QColor(Qt.white)) #self.qp.setPen(QColor(0x000ff41)) cur_line = 0 for line in lines: text_y_pos = self.rect().height() / 2 - ( len(lines) / 2) * self.qp.fontMetrics().height( ) + cur_line * self.qp.fontMetrics().height() # draw status self.qp.drawText(text_x_pos, text_y_pos, line) cur_line += 1 self.qp.setCompositionMode(cm) return def paint_status(self): a_offs = 20 base_x = self.rect_x + self.get_pixel_qty_per_line( ) * self.pixelSize + a_offs + 10 lines = [] lines.append("[Data]") lines.append(" Type: %s" % self.formatters[self.cur_formatter_idx][1]) lines.append(" Mode: %s" % self.composition_modes[self.cur_compos_mode][1]) cur_line = 1 text_x_pos = base_x + 10 self.qp.setPen(QColor(Qt.white)) for line in lines: text_y_pos = self.rect().height() - ( self.qp.fontMetrics().height() / 2) - (len(lines) - cur_line) * (self.qp.fontMetrics().height()) # draw status self.qp.drawText(text_x_pos, text_y_pos, line) cur_line += 1 # functions that can be called by filters # must no be called from within on_process_buffer() def on_filter_request_update(self, ea=None, center=True): if not ea: self.repaint() else: curea = self.get_address() if ea < curea or ea >= curea + self.get_pixel_qty(): # TODO: verify that ea is valid after following operation if center: ea -= int(self.get_pixel_qty() / 2) self.set_addr(ea) else: self.repaint() def on_filter_update_zoom(self, zoom): self.set_zoom(zoom) return def on_filter_update_zoom_delta(self, delta): self.set_zoom_delta(delta) return # end of functions that can be called by filters def get_filter_helptext(self): hlp = self.fm.help if not hlp: hlp = "No help available :[" jstfy = "\n" + 4 * " " hlp_fmt = jstfy + hlp.replace("\n", jstfy) helptxt = FILTER_HELP % (self.fm.name, hlp_fmt) return helptxt def keyPressEvent(self, event): if self.key is None: self.key = event.key() return def keyReleaseEvent(self, event): update = False key = event.key() modifiers = event.modifiers() shift_pressed = ((modifiers & Qt.ShiftModifier) == Qt.ShiftModifier) ctrl_pressed = ((modifiers & Qt.ControlModifier) == Qt.ControlModifier) if key == Qt.Key_F1 and ctrl_pressed: self.display_help_box(PLUGIN_HELP) self.repaint() elif key == Qt.Key_F2 and ctrl_pressed: self.display_help_box(self.get_filter_helptext(), isFilter=True) self.repaint() elif key == Qt.Key_G: addr = ida_kernwin.ask_addr(self.base + self.offs, 'Jump to address') if addr is not None: if self.sync: ida_kernwin.jumpto(addr) else: minea = ida_idaapi.get_inf_structure().get_minEA() maxea = ida_idaapi.get_inf_structure().get_maxEA() dst = min(max(addr, minea), maxea) self.set_addr(dst) elif key == Qt.Key_S: if not self.fm.lock_sync: self.set_sync_state(not self.get_sync_state()) update = True elif key == Qt.Key_D: self.cur_formatter_idx = (self.cur_formatter_idx + 1) % self.max_formatters self.repaint() elif key == Qt.Key_T: self.cur_compos_mode = (self.cur_compos_mode + 1) % len( self.composition_modes) self.repaint() elif key == Qt.Key_N: self.next_filter.emit() elif key == Qt.Key_B: self.prev_filter.emit() elif key == Qt.Key_F12 and shift_pressed and ctrl_pressed: img = self.paint_image(cursor=False) img = img.scaled(img.width() * self.pixelSize, img.height() * self.pixelSize, Qt.KeepAspectRatio, Qt.FastTransformation) done = False i = 0 while not done: fname = 'IDACyber_%04d.bmp' % i if not os.path.isfile(fname): if img.save(fname): ida_kernwin.msg('File exported to %s\n' % fname) else: ida_kernwin.warning( 'Error exporting screenshot to %s.' % fname) done = True i += 1 if i > 40: ida_kernwin.warning('Aborted. Error exporting screenshot.') break elif key == Qt.Key_PageDown: self.set_offset_delta(-self.get_pixel_qty()) update = True elif key == Qt.Key_PageUp: self.set_offset_delta(self.get_pixel_qty()) update = True elif key == Qt.Key_Down: if shift_pressed: self.set_offset_delta(-1) else: self.set_offset_delta(-self.get_pixel_qty_per_line()) update = True elif key == Qt.Key_Up: if shift_pressed: self.set_offset_delta(1) else: self.set_offset_delta(self.get_pixel_qty_per_line()) update = True elif key == Qt.Key_Plus: if ctrl_pressed: self.set_zoom_delta(1) update = True elif key == Qt.Key_Minus: if ctrl_pressed: self.set_zoom_delta(-1) update = True self.key = None if update: if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() self.statechanged.emit() self.repaint() return def wheelEvent(self, event): delta = round(event.angleDelta().y() / 120) # zoom if self.key == Qt.Key_Control: self.set_zoom_delta(delta) # width elif self.key == Qt.Key_X: if not self.lock_width: self.set_width_delta(delta) # offset (fine) elif self.key == Qt.Key_Shift: self.set_offset_delta(delta) if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() elif self.key == Qt.Key_H: if not self.lock_width: less = delta < 0 w = -8 if less else 8 self.set_pixel_qty_per_line((self.get_pixel_qty_per_line() & 0xFFFFFFF8) + w) # offset (coarse) else: self.set_offset_delta(delta * self.get_pixel_qty_per_line()) if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() self.statechanged.emit() self.repaint() return def mousePressEvent(self, event): x = event.pos().x() y = event.pos().y() within_graph = (x >= self.rect_x and x < self.rect_x + self.rect_x_width) self.is_dragging_graph = (within_graph and event.button() == Qt.LeftButton) return def mouseDoubleClickEvent(self, event): if self.link_pixel and event.button() == Qt.LeftButton: addr = self.base + self.offs + self._get_offs_by_pos(event.pos()) ida_kernwin.jumpto(addr) return def mouseReleaseEvent(self, event): if (event.button() == Qt.LeftButton and self.is_dragging_graph): self.is_dragging_graph = False self.prev_mouse_y = event.pos().y() self.fm.on_mb_click(event, self.get_address(), self.get_pixel_qty(), self.mouseOffs) if self.get_sync_state(): ida_kernwin.jumpto(self.base + self.offs) self.activateWindow() self.setFocus() self.statechanged.emit() return def mouseMoveEvent(self, event): x = event.pos().x() y = event.pos().y() within_graph = (x >= self.rect_x and x < self.rect_x + self.rect_x_width) """(sx1, sy1), (sx2, sy2) = self.slider_coords on_slider = (x >= sx1 and x< sx2 and y>= sy1 and y < sy2)""" update_state = self.is_dragging_graph or within_graph if self.is_dragging_graph: # zoom if self.key == Qt.Key_Control: self.set_zoom_delta(-1 if y > self.prev_mouse_y else 1) # width elif self.key == Qt.Key_X: if not self.lock_width: self.set_width_delta(-1 if y > self.prev_mouse_y else 1) elif self.key == Qt.Key_H: if not self.lock_width: less = y > self.prev_mouse_y delta = -16 if less else 16 self.set_pixel_qty_per_line((self.get_pixel_qty_per_line() & 0xFFFFFFF0) + delta) # scrolling (offset) elif y != self.prev_mouse_y: # offset (fine) delta = y - self.prev_mouse_y # offset (coarse) if self.key != Qt.Key_Shift: delta *= self.get_pixel_qty_per_line() self.set_offset_delta(delta) elif within_graph: self._update_mouse_coords(event.pos()) self.mouseOffs = self._get_offs_by_pos(event.pos()) if self.link_pixel and self.highlight_cursor: highlight_item( ida_bytes.get_item_head(self.get_cursor_address())) elif self.highlight_cursor: unhighlight_item() self.setToolTip( self.fm.on_get_tooltip(self.get_address(), self.get_pixel_qty(), self.mouseOffs)) if update_state: self.prev_mouse_y = y self.x = x self.statechanged.emit() self.repaint() return def set_sync_state(self, sync): self.sync = sync def get_sync_state(self): return self.sync def get_filter_idx(self): return self.filter_idx def set_filter(self, fltobj, idx): if self.fm: self.fm.on_deactivate() if fltobj: self.fm = fltobj """load filter config""" self.set_sync_state(self.fm.sync) self.lock_width = self.fm.lock_width self.set_pixel_qty_per_line(self.fm.width) self.lock_sync = self.fm.lock_sync self.show_address_range = self.fm.show_address_range # disabled for now # self.set_zoom(self.fm.zoom) self.link_pixel = self.fm.link_pixel self.highlight_cursor = self.fm.highlight_cursor self.statechanged.emit() """load filter config end""" self.fm.on_activate(idx) self.filter_idx = idx unhighlight_item() self.repaint() def set_addr(self, ea, new_cursor=None): _ea = ea selection, start, end = ida_kernwin.read_range_selection(None) if selection: _ea = start base = self.bh.get_base(_ea) self._set_base(base) self._set_offs(_ea - base) if new_cursor: self.set_cursor_offset(new_cursor) if self.highlight_cursor: highlight_item(_ea) self.repaint() def get_zoom(self): return self.pixelSize def set_zoom(self, zoom): self.pixelSize = zoom def set_zoom_delta(self, dzoom): self.set_zoom(max(1, self.pixelSize + dzoom)) return def get_pixel_qty_per_line(self): return self.maxPixelsPerLine def set_pixel_qty(self, qty): self.maxPixelsTotal = qty def get_pixel_qty(self): return self.maxPixelsTotal def get_address(self): return self.base + self.offs def get_cursor_address(self): return self.get_address() + self.mouseOffs def set_cursor_offset(self, ea): self.mouseOffs = ea - self.get_address() return def get_coords_by_address(self, address): base = self.get_address() # if address is visible in current window if address >= base and address < base + self.get_pixel_qty(): offs = address - base x = int(offs % self.get_pixel_qty_per_line()) y = int(offs / (self.get_pixel_qty_per_line())) return (x, y) return None def get_target_direction(self, address): base = self.get_address() # if address is visible in current window direction = None if address >= base and address < base + self.get_pixel_qty(): direction = 0 elif address < base: direction = 1 else: direction = 2 return direction def set_pixel_qty_per_line(self, width): self.maxPixelsPerLine = max(1, width) def set_width_delta(self, dwidth): self.maxPixelsPerLine = max(1, self.maxPixelsPerLine + dwidth) def set_offset_delta(self, doffs): newea = self.base + self.offs - doffs minea = ida_idaapi.get_inf_structure().get_minEA() maxea = ida_idaapi.get_inf_structure().get_maxEA() if doffs < 0: delta = doffs if newea < maxea else doffs - (maxea - newea) else: delta = doffs if newea >= minea else doffs - (minea - newea) self._set_offs(self.offs - delta) def _get_offs_by_pos(self, pos): elemX = int(self.get_elem_x()) elemY = int(self.get_elem_y()) offs = elemY * self.get_pixel_qty_per_line() + elemX return offs def _update_mouse_coords(self, pos): x = pos.x() y = pos.y() self.mouse_abs_x = x self.mouse_abs_y = y self.elemX = max( 0, min(((max(0, x - self.rect_x)) / self.pixelSize), self.get_pixel_qty_per_line() - 1)) self.elemY = min( y / self.pixelSize, (self.get_pixel_qty() / self.get_pixel_qty_per_line()) - 1) def get_elem_x(self): return self.elemX def get_elem_y(self): return self.elemY def _set_offs(self, offs): self.offs = offs def _set_base(self, ea): self.base = ea