diff options
Diffstat (limited to 'python/examples')
| -rw-r--r-- | python/examples/triage/__init__.py | 2 | ||||
| -rw-r--r-- | python/examples/triage/byte.py | 792 | ||||
| -rw-r--r-- | python/examples/triage/entropy.py | 40 | ||||
| -rw-r--r-- | python/examples/triage/exports.py | 123 | ||||
| -rw-r--r-- | python/examples/triage/files.py | 140 | ||||
| -rw-r--r-- | python/examples/triage/headers.py | 91 | ||||
| -rw-r--r-- | python/examples/triage/imports.py | 37 | ||||
| -rw-r--r-- | python/examples/triage/sections.py | 147 | ||||
| -rw-r--r-- | python/examples/triage/view.py | 134 |
9 files changed, 1438 insertions, 68 deletions
diff --git a/python/examples/triage/__init__.py b/python/examples/triage/__init__.py index 90cdbd77..d43379da 100644 --- a/python/examples/triage/__init__.py +++ b/python/examples/triage/__init__.py @@ -1 +1,3 @@ from . import view +from . import byte +from . import files diff --git a/python/examples/triage/byte.py b/python/examples/triage/byte.py new file mode 100644 index 00000000..23302033 --- /dev/null +++ b/python/examples/triage/byte.py @@ -0,0 +1,792 @@ +# coding: utf8 + +from PySide2.QtWidgets import QAbstractScrollArea, QAbstractSlider +from PySide2.QtGui import QPainter, QPalette, QFont +from PySide2.QtCore import Qt, QTimer, QRect +import binaryninjaui +from binaryninjaui import View, ViewType, RenderContext, UIContext, ThemeColor, UIAction +from binaryninja.enums import LinearDisassemblyLineType +from binaryninja.binaryview import AddressRange + + +class ByteViewLine(object): + def __init__(self, addr, length, text, separator): + self.address = addr + self.length = length + self.text = text + self.separator = separator + + +class ByteView(QAbstractScrollArea, View): + def __init__(self, parent, data): + QAbstractScrollArea.__init__(self, parent) + View.__init__(self) + self.setupView(self) + self.data = data + self.byte_mapping = [ + u' ', u'☺', u'☻', u'♥', u'♦', u'♣', u'♠', u'•', u'◘', u'○', u'◙', u'♂', u'♀', u'♪', u'♫', u'☼', + u'▸', u'◂', u'↕', u'‼', u'¶', u'§', u'▬', u'↨', u'↑', u'↓', u'→', u'←', u'∟', u'↔', u'▴', u'▾', + u' ', u'!', u'"', u'#', u'$', u'%', u'&', u'\'', u'(', u')', u'*', u'+', u',', u'-', u'.', u'/', + u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u':', u';', u'<', u'=', u'>', u'?', + u'@', u'A', u'B', u'C', u'D', u'E', u'F', u'G', u'H', u'I', u'J', u'K', u'L', u'M', u'N', u'O', + u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W', u'X', u'Y', u'Z', u'[', u'\\', u']', u'^', u'_', + u'`', u'a', u'b', u'c', u'd', u'e', u'f', u'g', u'h', u'i', u'j', u'k', u'l', u'm', u'n', u'o', + u'p', u'q', u'r', u's', u't', u'u', u'v', u'w', u'x', u'y', u'z', u'{', u'|', u'}', u'~', u'⌂', + u'Ç', u'ü', u'é', u'â', u'ä', u'à', u'å', u'ç', u'ê', u'ë', u'è', u'ï', u'î', u'ì', u'Ä', u'Å', + u'É', u'æ', u'Æ', u'ô', u'ö', u'ò', u'û', u'ù', u'ÿ', u'Ö', u'Ü', u'¢', u'£', u'¥', u'₧', u'ƒ', + u'á', u'í', u'ó', u'ú', u'ñ', u'Ñ', u'ª', u'º', u'¿', u'⌐', u'¬', u'½', u'¼', u'¡', u'«', u'»', + u'░', u'▒', u'▓', u'│', u'┤', u'╡', u'╢', u'╖', u'╕', u'╣', u'║', u'╗', u'╝', u'╜', u'╛', u'┐', + u'└', u'┴', u'┬', u'├', u'─', u'┼', u'╞', u'╟', u'╚', u'╔', u'╩', u'╦', u'╠', u'═', u'╬', u'╧', + u'╨', u'╤', u'╥', u'╙', u'╘', u'╒', u'╓', u'╫', u'╪', u'┘', u'┌', u'█', u'▄', u'▌', u'▐', u'▀', + u'α', u'ß', u'Γ', u'π', u'Σ', u'σ', u'µ', u'τ', u'Φ', u'Θ', u'Ω', u'δ', u'∞', u'φ', u'ε', u'∩', + u'≡', u'±', u'≥', u'≤', u'⌠', u'⌡', u'÷', u'≈', u'°', u'∙', u'·', u'√', u'ⁿ', u'²', u'■', u' ' + ] + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + self.setFocusPolicy(Qt.StrongFocus) + + self.cursorAddr = self.data.start + self.prevCursorAddr = self.cursorAddr + self.selectionStartAddr = self.cursorAddr + self.topAddr = self.cursorAddr + self.topLine = 0 + self.selectionVisible = False + self.caretVisible = False + self.caretBlink = True + self.leftButtonDown = False + self.cols = 128 + self.updatesRequired = False + self.visibleRows = 1 + self.lines = [] + + self.updateRanges() + + areaSize = self.viewport().size() + self.adjustSize(areaSize.width(), areaSize.height()) + + if self.allocatedLength > 0x7fffffff: + self.scrollBarMultiplier = (self.allocatedLength // 0x7fffffff) + 1 + else: + self.scrollBarMultiplier = 1 + self.wheelDelta = 0 + self.updatingScrollBar = False + self.verticalScrollBar().setRange(0, (self.allocatedLength - 1) // self.scrollBarMultiplier) + self.verticalScrollBar().sliderMoved.connect(self.scrollBarMoved) + self.verticalScrollBar().actionTriggered.connect(self.scrollBarAction) + + self.cursorTimer = QTimer(self) + self.cursorTimer.setInterval(500) + self.cursorTimer.setSingleShot(False) + self.cursorTimer.timeout.connect(self.cursorTimerEvent) + self.cursorTimer.start() + + self.updateTimer = QTimer(self) + self.updateTimer.setInterval(200) + self.updateTimer.setSingleShot(False) + #self.updateTimer.timeout.connect(self.updateTimerEvent) + + self.actionHandler().bindAction("Move Cursor Up", UIAction(lambda ctxt: self.up(False))) + self.actionHandler().bindAction("Move Cursor Down", UIAction(lambda ctxt: self.down(False))) + self.actionHandler().bindAction("Move Cursor Left", UIAction(lambda ctxt: self.left(1, False))) + self.actionHandler().bindAction("Move Cursor Right", UIAction(lambda ctxt: self.right(1, False))) + self.actionHandler().bindAction("Move Cursor Word Left", UIAction(lambda ctxt: self.left(8, False))) + self.actionHandler().bindAction("Move Cursor Word Right", UIAction(lambda ctxt: self.right(8, False))) + self.actionHandler().bindAction("Extend Selection Up", UIAction(lambda ctxt: self.up(True))) + self.actionHandler().bindAction("Extend Selection Down", UIAction(lambda ctxt: self.down(True))) + self.actionHandler().bindAction("Extend Selection Left", UIAction(lambda ctxt: self.left(1, True))) + self.actionHandler().bindAction("Extend Selection Right", UIAction(lambda ctxt: self.right(1, True))) + self.actionHandler().bindAction("Extend Selection Word Left", UIAction(lambda ctxt: self.left(8, True))) + self.actionHandler().bindAction("Extend Selection Word Right", UIAction(lambda ctxt: self.right(8, True))) + self.actionHandler().bindAction("Page Up", UIAction(lambda ctxt: self.pageUp(False))) + self.actionHandler().bindAction("Page Down", UIAction(lambda ctxt: self.pageDown(False))) + self.actionHandler().bindAction("Extend Selection Page Up", UIAction(lambda ctxt: self.pageUp(True))) + self.actionHandler().bindAction("Extend Selection Page Down", UIAction(lambda ctxt: self.pageDown(True))) + self.actionHandler().bindAction("Move Cursor to Start Of Line", UIAction(lambda ctxt: self.moveToStartOfLine(False))) + self.actionHandler().bindAction("Move Cursor to End Of Line", UIAction(lambda ctxt: self.moveToEndOfLine(False))) + self.actionHandler().bindAction("Move Cursor to Start Of View", UIAction(lambda ctxt: self.moveToStartOfView(False))) + self.actionHandler().bindAction("Move Cursor to End Of View", UIAction(lambda ctxt: self.moveToEndOfView(False))) + self.actionHandler().bindAction("Extend Selection to Start Of Line", UIAction(lambda ctxt: self.moveToStartOfLine(True))) + self.actionHandler().bindAction("Extend Selection to End Of Line", UIAction(lambda ctxt: self.moveToEndOfLine(True))) + self.actionHandler().bindAction("Extend Selection to Start Of View", UIAction(lambda ctxt: self.moveToStartOfView(True))) + self.actionHandler().bindAction("Extend Selection to End Of View", UIAction(lambda ctxt: self.moveToEndOfView(True))) + + def getData(self): + return self.data + + def getStart(self): + return self.data.start + + def getEnd(self): + return self.data.end + + def getLength(self): + return self.getEnd() - self.getStart() + + def getCurrentOffset(self): + return self.cursorAddr + + def getSelectionOffsets(self): + start = self.selectionStartAddr + end = self.cursorAddr + if end < start: + t = start + start = end + end = t + return (start, end) + + def updateRanges(self): + self.ranges = self.data.allocated_ranges + # Remove regions not backed by the file + for i in self.data.segments: + if i.data_length < len(i): + self.removeRange(i.start + i.data_length, i.end) + self.allocatedLength = 0 + for i in self.ranges: + self.allocatedLength += i.end - i.start + + def removeRange(self, begin, end): + newRanges = [] + for i in self.ranges: + if (end <= i.start) or (begin >= i.end): + newRanges.append(i) + elif (begin <= i.start) and (end >= i.end): + continue + elif (begin <= i.start) and (end < i.end): + newRanges.append(AddressRange(end, i.end)) + elif (begin > i.start) and (end >= i.end): + newRanges.append(AddressRange(i.start, begin)) + else: + newRanges.append(AddressRange(i.start, begin)) + newRanges.append(AddressRange(end, i.end)) + self.ranges = newRanges + + def setTopToAddress(self, addr): + for i in self.ranges: + if (addr >= i.start) and (addr <= i.end): + self.topAddr = addr - ((addr - i.start) % self.cols) + if self.topAddr < i.start: + self.topAddr = i.start + return + if i.start > addr: + self.topAddr = i.start + return + self.topAddr = self.data.end + + def navigate(self, addr): + if addr < self.getStart(): + return False + if addr > self.getEnd(): + return False + self.cursorAddr = self.getStart() + for i in self.ranges: + if i.start > addr: + break + if addr > i.end: + self.cursorAddr = i.end + elif addr >= i.start: + self.cursorAddr = addr + else: + self.cursorAddr = i.start + self.setTopToAddress(self.cursorAddr) + self.refreshLines() + self.showContextAroundTop() + self.selectNone() + self.repositionCaret() + return True + + def updateFonts(self): + areaSize = self.viewport().size() + self.adjustSize(areaSize.width(), areaSize.height()) + + def createRenderContext(self): + render = RenderContext(self) + userFont = binaryninjaui.getMonospaceFont(self) + # Some fonts aren't fixed width across all characters, use a known good one + font = QFont("Menlo", userFont.pointSize()) + font.setKerning(False) + render.setFont(font) + return render + + def adjustSize(self, width, height): + self.addrWidth = max(len("%x" % self.data.end), 8) + render = self.createRenderContext() + cols = ((width - 4) // render.getFontWidth()) - (self.addrWidth + 2) + if cols != self.cols: + self.cols = cols + if self.topLine < len(self.lines): + self.setTopToAddress(self.lines[self.topLine].address) + else: + self.setTopToAddress(self.cursorAddr) + self.refreshLines() + self.visibleRows = (height - 4) // render.getFontHeight() + self.verticalScrollBar().setPageStep(self.visibleRows * self.cols // self.scrollBarMultiplier) + self.refreshAtCurrentLocation() + self.viewport().update() + + def getContiguousOffsetForAddress(self, addr): + offset = 0 + for i in self.ranges: + if (addr >= i.start) and (addr <= i.end): + offset += addr - i.start + break + offset += i.end - i.start + return offset + + def getAddressForContiguousOffset(self, offset): + cur = 0 + for i in self.ranges: + if offset < (cur + (i.end - i.start)): + return i.start + (offset - cur) + cur += i.end - i.start + return self.data.end + + def refreshLines(self): + addr = self.topAddr + self.lines = [] + self.topLine = 0 + self.bottomAddr = self.topAddr + self.updateRanges() + if self.allocatedLength > 0x7fffffff: + self.scrollBarMultiplier = (self.allocatedLength // 0x7fffffff) + 1 + else: + self.scrollBarMultiplier = 1 + self.updatingScrollBar = True + self.verticalScrollBar().setRange(0, (self.allocatedLength - 1) // self.scrollBarMultiplier) + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(addr) // self.scrollBarMultiplier) + self.updatingScrollBar = False + self.updateCache() + self.viewport().update() + UIContext.updateStatus() + + def refreshAtCurrentLocation(self): + if self.topLine < len(self.lines): + self.topAddr = self.lines[self.topLine].address + self.refreshLines() + + def createLine(self, addr, length, separator): + if separator: + return ByteViewLine(addr, length, u'', True) + else: + data = self.data.read(addr, length) + text = u''.join([self.byte_mapping[value] for value in data]) + return ByteViewLine(addr, length, text, False) + + def cachePreviousLines(self): + prevEnd = None + for i in self.ranges: + if (self.topAddr > i.start) and (self.topAddr <= i.end): + startLine = self.topAddr - ((self.topAddr - i.start) % self.cols) + if startLine == self.topAddr: + startLine -= self.cols + if startLine < i.start: + startLine = i.start + line = self.createLine(startLine, self.topAddr - startLine, False) + self.lines.insert(0, line) + self.topLine += 1 + self.topAddr = startLine + return True + elif i.start >= self.topAddr: + if prevEnd is None: + return False + line = self.createLine(prevEnd, i.start - prevEnd, True) + self.lines.insert(0, line) + self.topLine += 1 + self.topAddr = prevEnd + return True + prevEnd = i.end + if prevEnd is None: + return False + line = self.createLine(prevEnd, self.topAddr - prevEnd, True) + self.lines.insert(0, line) + self.topLine += 1 + self.topAddr = prevEnd + + def cacheNextLines(self): + lastAddr = self.data.start + for i in self.ranges: + if (self.bottomAddr >= i.start) and (self.bottomAddr < i.end): + endLine = self.bottomAddr + self.cols + if endLine > i.end: + endLine = i.end + line = self.createLine(self.bottomAddr, endLine - self.bottomAddr, False) + self.lines.append(line) + self.bottomAddr = endLine + return True + elif i.start > self.bottomAddr: + line = self.createLine(self.bottomAddr, i.start - self.bottomAddr, True) + self.lines.append(line) + self.bottomAddr = i.start + return True + lastAddr = i.end + if self.bottomAddr == lastAddr: + # Ensure there is a place for the cursor at the end of the file + if (len(self.lines) > 0) and (self.lines[-1].length != self.cols): + return False + line = self.createLine(lastAddr, 0, False) + self.lines.append(line) + self.bottomAddr += 1 + return True + return False + + def updateCache(self): + # Cache enough for the current page and the next page + while (len(self.lines) - self.topLine) <= (self.visibleRows * 2): + if not self.cacheNextLines(): + break + # Cache enough for the previous page + while self.topLine <= self.visibleRows: + if not self.cachePreviousLines(): + break + # Trim cache + if self.topLine > (self.visibleRows * 4): + self.lines = self.lines[self.topLine - (self.visibleRows * 4):] + self.topLine = self.visibleRows * 4 + self.topAddr = self.lines[0].address + if (len(self.lines) - self.topLine) > (self.visibleRows * 5): + self.bottomAddr = self.lines[self.topLine + (self.visibleRows * 5)].address + self.lines = self.lines[0:self.topLine + (self.visibleRows * 5)] + + def scrollLines(self, count): + newOffset = self.topLine + count + if newOffset < 0: + self.topLine = 0 + elif newOffset >= len(self.lines): + self.topLine = len(self.lines) - 1 + else: + self.topLine = newOffset + self.updateCache() + self.viewport().update() + if self.topLine < len(self.lines): + self.updatingScrollBar = True + addr = self.lines[self.topLine].address + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(addr) // self.scrollBarMultiplier) + self.updatingScrollBar = False + + def showContextAroundTop(self): + scroll = self.visibleRows // 4 + if scroll > self.topLine: + self.topLine = 0 + else: + self.topLine -= scroll + if self.topLine < len(self.lines): + self.updatingScrollBar = True + addr = self.lines[self.topLine].address + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(addr) // self.scrollBarMultiplier) + self.updatingScrollBar = False + self.updateCache() + + def repositionCaret(self): + self.updateCache() + found = False + for i in range(0, len(self.lines)): + if (((self.cursorAddr >= self.lines[i].address) and (self.cursorAddr < (self.lines[i].address + self.lines[i].length))) or + (((i + 1) == len(self.lines)) and (self.cursorAddr == (self.lines[i].address + self.lines[i].length)))): + if i < self.topLine: + self.topLine = i + elif i > (self.topLine + self.visibleRows - 1): + self.topLine = i - (self.visibleRows - 1) + self.updatingScrollBar = True + addr = self.lines[self.topLine].address + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(addr) // self.scrollBarMultiplier) + self.updatingScrollBar = False + self.updateCache() + self.viewport().update() + found = True + break + if not found: + self.setTopToAddress(self.cursorAddr) + self.refreshLines() + self.showContextAroundTop() + # Force caret to be visible and repaint + self.caretBlink = True + self.cursorTimer.stop() + self.cursorTimer.start() + self.updateCaret() + UIContext.updateStatus() + + def updateCaret(self): + # Rerender both the old caret position and the new caret position + render = self.createRenderContext() + for i in range(self.topLine, min(len(self.lines), self.topLine + self.visibleRows)): + if (((self.prevCursorAddr >= self.lines[i].address) and (self.prevCursorAddr <= (self.lines[i].address + self.lines[i].length))) or + ((self.cursorAddr >= self.lines[i].address) and (self.cursorAddr <= (self.lines[i].address + self.lines[i].length)))): + self.viewport().update(0, (i - self.topLine) * render.getFontHeight(), + self.viewport().size().width(), render.getFontHeight() + 3) + + def resizeEvent(self, event): + self.adjustSize(event.size().width(), event.size().height()) + + def paintEvent(self, event): + p = QPainter(self.viewport()) + render = self.createRenderContext() + render.init(p) + charWidth = render.getFontWidth() + charHeight = render.getFontHeight() + + # Compute range that needs to be updated + topY = event.rect().y() + botY = topY + event.rect().height() + topY = (topY - 2) // charHeight + botY = ((botY - 2) // charHeight) + 1 + + # Compute selection range + selection = False + selStart, selEnd = self.getSelectionOffsets() + if selStart != selEnd: + selection = True + + # Draw selection + if selection: + startY = None + endY = None + startX = None + endX = None + for i in range(0, len(self.lines)): + if selStart >= self.lines[i].address: + startY = i - self.topLine + startX = selStart - self.lines[i].address + if startX > self.cols: + startX = self.cols + if selEnd >= self.lines[i].address: + endY = i - self.topLine + endX = selEnd - self.lines[i].address + if endX > self.cols: + endX = self.cols + + if startY is not None and endY is not None: + p.setPen(binaryninjaui.getThemeColor(ThemeColor.SelectionColor)) + p.setBrush(binaryninjaui.getThemeColor(ThemeColor.SelectionColor)) + if startY == endY: + p.drawRect(2 + (self.addrWidth + 2 + startX) * charWidth, 2 + startY * charHeight, + (endX - startX) * charWidth, charHeight + 1) + else: + p.drawRect(2 + (self.addrWidth + 2 + startX) * charWidth, 2 + startY * charHeight, + (self.cols - startX) * charWidth, charHeight + 1) + if endX > 0: + p.drawRect(2 + (self.addrWidth + 2) * charWidth, 2 + endY * charHeight, + endX * charWidth, charHeight + 1) + if (endY - startY) > 1: + p.drawRect(2 + (self.addrWidth + 2) * charWidth, 2 + (startY + 1) * charHeight, + self.cols * charWidth, ((endY - startY) - 1) * charHeight + 1) + + # Paint each line + color = self.palette().color(QPalette.WindowText) + for y in range(topY, botY): + if (y + self.topLine) < 0: + continue + if (y + self.topLine) >= len(self.lines): + break + if self.lines[y + self.topLine].separator: + render.drawLinearDisassemblyLineBackground(p, LinearDisassemblyLineType.NonContiguousSeparatorLineType, + QRect(0, 2 + y * charHeight, event.rect().width(), charHeight), 0) + continue + + lineStartAddr = self.lines[y + self.topLine].address + addrStr = "%.8x" % lineStartAddr + length = self.lines[y + self.topLine].length + text = self.lines[y + self.topLine].text + + cursorCol = None + if (((self.cursorAddr >= lineStartAddr) and (self.cursorAddr < (lineStartAddr + length))) or + (((y + self.topLine + 1) >= len(self.lines)) and (self.cursorAddr == (lineStartAddr + length)))): + cursorCol = self.cursorAddr - lineStartAddr + + render.drawText(p, 2, 2 + y * charHeight, binaryninjaui.getThemeColor(ThemeColor.AddressColor), addrStr) + render.drawText(p, 2 + (self.addrWidth + 2) * charWidth, 2 + y * charHeight, color, text) + + if self.caretVisible and self.caretBlink and not selection and cursorCol is not None: + p.setPen(Qt.NoPen) + p.setBrush(self.palette().color(QPalette.WindowText)) + p.drawRect(2 + (self.addrWidth + 2 + cursorCol) * charWidth, 2 + y * charHeight, charWidth, charHeight + 1) + caretTextColor = self.palette().color(QPalette.Base) + byteValue = self.data.read(lineStartAddr + cursorCol, 1) + if len(byteValue) == 1: + byteStr = self.byte_mapping[ord(byteValue)] + render.drawText(p, 2 + (self.addrWidth + 2 + cursorCol) * charWidth, 2 + y * charHeight, caretTextColor, byteStr) + + def wheelEvent(self, event): + if event.orientation() == Qt.Horizontal: + return + self.wheelDelta -= event.delta() + if (self.wheelDelta <= -40) or (self.wheelDelta >= 40): + lines = self.wheelDelta // 40 + self.wheelDelta -= lines * 40 + self.scrollLines(lines) + + def scrollBarMoved(self, value): + if self.updatingScrollBar: + return + self.wheelDelta = 0 + addr = self.getAddressForContiguousOffset(value * self.scrollBarMultiplier) + self.setTopToAddress(addr) + self.refreshLines() + for i in range(1, len(self.lines)): + if (self.lines[i].address + self.lines[i].length) > addr: + self.topLine = i - 1 + break + self.updateCache() + + def scrollBarAction(self, action): + if action == QAbstractSlider.SliderSingleStepAdd: + self.wheelDelta = 0 + self.scrollLines(1) + elif action == QAbstractSlider.SliderSingleStepSub: + self.wheelDelta = 0 + self.scrollLines(-1) + elif action == QAbstractSlider.SliderPageStepAdd: + self.wheelDelta = 0 + self.scrollLines(self.visibleRows) + elif action == QAbstractSlider.SliderPageStepSub: + self.wheelDelta = 0 + self.scrollLines(-self.visibleRows) + elif action == QAbstractSlider.SliderToMinimum: + self.wheelDelta = 0 + self.setTopToAddress(self.getStart()) + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(self.topAddr) // self.scrollBarMultiplier) + self.refreshLines() + elif action == QAbstractSlider.SliderToMaximum: + self.wheelDelta = 0 + self.setTopToAddress(self.getEnd()) + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(self.topAddr) // self.scrollBarMultiplier) + self.refreshLines() + + def cursorTimerEvent(self): + self.caretBlink = not self.caretBlink + self.updateCaret() + + def focusInEvent(self, event): + self.caretVisible = True + self.updateCaret() + + def focusOutEvent(self, event): + self.caretVisible = False + self.leftButtonDown = False + self.updateCaret() + + def selectNone(self): + for i in self.lines: + if (self.cursorAddr >= i.address) and (self.cursorAddr < (i.address + i.length)) and i.separator: + self.cursorAddr = i.address + i.length + break + self.selectionStartAddr = self.cursorAddr + if self.selectionVisible: + self.viewport().update() + self.repositionCaret() + UIContext.updateStatus() + + def selectAll(self): + self.selectionStartAddr = self.getStart() + self.cursorAddr = self.getEnd() + self.viewport().update() + UIContext.updateStatus() + + def adjustAddressAfterBackwardMovement(self): + lastAddr = self.getStart() + for i in self.ranges: + if (self.cursorAddr >= i.start) and (self.cursorAddr < i.end): + break + if i.start > self.cursorAddr: + self.cursorAddr = lastAddr + break + lastAddr = i.end - 1 + + def adjustAddressAfterForwardMovement(self): + for i in self.ranges: + if (self.cursorAddr >= i.start) and (self.cursorAddr < i.end): + break + if i.start > self.cursorAddr: + self.cursorAddr = i.start + break + + def left(self, count, selecting): + if self.cursorAddr > (self.getStart() + count): + self.cursorAddr -= count + else: + self.cursorAddr = self.getStart() + self.adjustAddressAfterBackwardMovement() + if not selecting: + self.selectNone() + self.repositionCaret() + if self.selectionVisible or selecting: + self.viewport().update() + + def right(self, count, selecting): + if self.cursorAddr <= (self.getEnd() - count): + self.cursorAddr += count + else: + self.cursorAddr = self.getEnd() + self.adjustAddressAfterForwardMovement() + if not selecting: + self.selectNone() + self.repositionCaret() + if self.selectionVisible or selecting: + self.viewport().update() + + def up(self, selecting): + self.left(self.cols, selecting) + + def down(self, selecting): + self.right(self.cols, selecting) + + def pageUp(self, selecting): + for i in range(0, len(self.lines)): + if (((self.cursorAddr >= self.lines[i].address) and (self.cursorAddr < (self.lines[i].address + self.lines[i].length))) or + (((i + 1) == len(self.lines)) and (self.cursorAddr == (self.lines[i].address + self.lines[i].length)))): + if i < self.visibleRows: + self.cursorAddr = self.getStart() + else: + lineOfs = self.cursorAddr - self.lines[i].address + self.cursorAddr = self.lines[i - self.visibleRows].address + lineOfs + if self.cursorAddr < self.lines[i - self.visibleRows].address: + self.cursorAddr = self.lines[i - self.visibleRows].address + elif self.cursorAddr >= (self.lines[i - self.visibleRows].address + self.lines[i - self.visibleRows].length): + self.cursorAddr = self.lines[i - self.visibleRows].address + self.lines[i - self.visibleRows].length - 1 + self.adjustAddressAfterBackwardMovement() + if self.topLine > self.visibleRows: + self.topLine -= self.visibleRows + else: + self.topLine = 0 + if self.topLine < len(self.lines): + self.updatingScrollBar = True + addr = self.lines[self.topLine].address + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(addr) // self.scrollBarMultiplier) + self.updatingScrollBar = False + if not selecting: + self.selectNone() + self.repositionCaret() + self.viewport().update() + + def pageDown(self, selecting): + for i in range(0, len(self.lines)): + if (((self.cursorAddr >= self.lines[i].address) and (self.cursorAddr < (self.lines[i].address + self.lines[i].length))) or + (((i + 1) == len(self.lines)) and (self.cursorAddr == (self.lines[i].address + self.lines[i].length)))): + if i >= (len(self.lines) - self.visibleRows): + self.cursorAddr = self.getEnd() + else: + lineOfs = self.cursorAddr - self.lines[i].address + self.cursorAddr = self.lines[i + self.visibleRows].address + lineOfs + if self.cursorAddr < self.lines[i + self.visibleRows].address: + self.cursorAddr = self.lines[i + self.visibleRows].address + elif self.cursorAddr >= (self.lines[i + self.visibleRows].address + self.lines[i - self.visibleRows].length): + self.cursorAddr = self.lines[i + self.visibleRows].address + self.lines[i - self.visibleRows].length - 1 + self.adjustAddressAfterForwardMovement() + if (self.topLine + self.visibleRows) < len(self.lines): + self.topLine += self.visibleRows + elif len(self.lines) > 0: + self.topLine = len(self.lines) - 1 + if self.topLine < len(self.lines): + self.updatingScrollBar = True + addr = self.lines[self.topLine].address + self.verticalScrollBar().setValue(self.getContiguousOffsetForAddress(addr) // self.scrollBarMultiplier) + self.updatingScrollBar = False + if not selecting: + self.selectNone() + self.repositionCaret() + self.viewport().update() + + def moveToStartOfLine(self, selecting): + for i in self.lines: + if (self.cursorAddr >= i.address) and (self.cursorAddr < (i.address + i.length)): + self.cursorAddr = i.address + break + if not selecting: + self.selectNone() + self.repositionCaret() + if self.selectionVisible or selecting: + self.viewport().update() + + def moveToEndOfLine(self, selecting): + for i in self.lines: + if (self.cursorAddr >= i.address) and (self.cursorAddr < (i.address + i.length)): + self.cursorAddr = i.address + i.length - 1 + break + if not selecting: + self.selectNone() + self.repositionCaret() + if self.selectionVisible or selecting: + self.viewport().update() + + def moveToStartOfView(self, selecting): + self.cursorAddr = self.getStart() + if not selecting: + self.selectNone() + self.repositionCaret() + if self.selectionVisible or selecting: + self.viewport().update() + + def moveToEndOfView(self, selecting): + self.cursorAddr = self.getEnd() + if not selecting: + self.selectNone() + self.repositionCaret() + if self.selectionVisible or selecting: + self.viewport().update() + + def addressFromLocation(self, x, y): + if y < 0: + y = 0 + if x < 0: + x = 0 + if x > self.cols: + x = self.cols + if (y + self.topLine) >= len(self.lines): + return self.getEnd() + if self.lines[y + self.topLine].separator: + return self.lines[y + self.topLine].address - 1 + result = self.lines[y + self.topLine].address + x + if result >= (self.lines[y + self.topLine].address + self.lines[y + self.topLine].length): + if (y + self.topLine) == (len(self.lines) - 1): + return self.getEnd() + else: + return self.lines[y + self.topLine].address + self.lines[y + self.topLine].length - 1 + return result + + def mousePressEvent(self, event): + if event.button() != Qt.LeftButton: + return + render = self.createRenderContext() + x = (event.x() - 2) // render.getFontWidth() - (self.addrWidth + 2) + y = (event.y() - 2) // render.getFontHeight() + self.lastMouseX = x + self.lastMouseY = y + self.cursorAddr = self.addressFromLocation(x, y) + if (event.modifiers() & Qt.ShiftModifier) == 0: + self.selectNone() + self.repositionCaret() + if (event.modifiers() & Qt.ShiftModifier) != 0: + self.viewport().update() + self.leftButtonDown = True + + def mouseMoveEvent(self, event): + if not self.leftButtonDown: + return + render = self.createRenderContext() + x = (event.x() - 2) // render.getFontWidth() - (self.addrWidth + 2) + y = (event.y() - 2) // render.getFontHeight() + if (x == self.lastMouseX) and (y == self.lastMouseY): + return + self.lastMouseX = x + self.lastMouseY = y + self.cursorAddr = self.addressFromLocation(x, y) + self.repositionCaret() + self.viewport().update() + + def mouseReleaseEvent(self, event): + if event.button() != Qt.LeftButton: + return + self.leftButtonDown = False + + +class ByteViewType(ViewType): + def __init__(self): + super(ByteViewType, self).__init__("Bytes", "Byte Overview") + + def getPriority(self, data, filename): + return 1 + + def create(self, data, view_frame): + return ByteView(view_frame, data) + + +ViewType.registerViewType(ByteViewType()) diff --git a/python/examples/triage/entropy.py b/python/examples/triage/entropy.py index 03249968..37ce295a 100644 --- a/python/examples/triage/entropy.py +++ b/python/examples/triage/entropy.py @@ -3,7 +3,8 @@ import threading from PySide2.QtWidgets import QWidget from PySide2.QtGui import QImage, QColor, QPainter from PySide2.QtCore import Qt, QSize, QTimer -from binaryninjaui import ViewFrame +import binaryninjaui +from binaryninjaui import ViewFrame, ThemeColor class EntropyThread(threading.Thread): @@ -17,30 +18,22 @@ class EntropyThread(threading.Thread): def run(self): width = self.image.width() for i in range(0, width): - block = self.data.read(self.data.start + i * self.block_size, self.block_size) - if len(block) == 0: - v = 0 - else: - dist = [0] * 0x100 - for j in range(0, len(block)): - value = ord(block[j:j+1]) - dist[value] += 1 - s = 0 - for j in range(0, 256): - if dist[j] != 0: - s += (float(dist[j]) / len(block)) * math.log(float(dist[j]) / len(block)) - s = s / math.log(1 / 256.0) - v = int(s * 255) + v = int(self.data.get_entropy(self.data.start + i * self.block_size, self.block_size)[0] * 255) if v >= 240: - self.image.setPixelColor(i, 0, QColor(v, v, v / 4, 255)) + color = binaryninjaui.getThemeColor(ThemeColor.YellowStandardHighlightColor) + self.image.setPixelColor(i, 0, color) else: - self.image.setPixelColor(i, 0, QColor(v / 4, v / 4, v, 255)) + baseColor = binaryninjaui.getThemeColor(ThemeColor.FeatureMapBaseColor) + entropyColor = binaryninjaui.getThemeColor(ThemeColor.BlueStandardHighlightColor) + color = binaryninjaui.mixColor(baseColor, entropyColor, v) + self.image.setPixelColor(i, 0, color) self.updated = True class EntropyWidget(QWidget): - def __init__(self, parent, data): + def __init__(self, parent, view, data): super(EntropyWidget, self).__init__(parent) + self.view = view self.data = data self.raw_data = data.file.raw @@ -54,7 +47,7 @@ class EntropyWidget(QWidget): self.timer = QTimer() self.timer.timeout.connect(self.timerEvent) - self.timer.setInterval(250) + self.timer.setInterval(100) self.timer.setSingleShot(False) self.timer.start() @@ -81,11 +74,4 @@ class EntropyWidget(QWidget): return frac = float(event.x()) / self.rect().width() offset = int(frac * self.width * self.block_size) - addr = self.data.get_address_for_data_offset(offset) - view_frame = ViewFrame.viewFrameForWidget(self) - if view_frame is None: - return - if addr is None: - view_frame.navigate("Hex:Raw", offset) - else: - view_frame.navigate("Linear:" + view_frame.getCurrentDataType(), addr) + self.view.navigateToFileOffset(offset) diff --git a/python/examples/triage/exports.py b/python/examples/triage/exports.py new file mode 100644 index 00000000..0e2239e0 --- /dev/null +++ b/python/examples/triage/exports.py @@ -0,0 +1,123 @@ +from PySide2.QtWidgets import QTreeView +from PySide2.QtCore import Qt, QAbstractItemModel, QModelIndex, QSize +from binaryninja.enums import SymbolType, SymbolBinding +import binaryninjaui +from binaryninjaui import ViewFrame + + +class GenericExportsModel(QAbstractItemModel): + def __init__(self, data): + super(GenericExportsModel, self).__init__() + self.entries = [] + self.addr_col = 0 + self.name_col = 1 + self.ordinal_col = None + self.total_cols = 2 + for sym in data.get_symbols_of_type(SymbolType.FunctionSymbol): + if sym.binding == SymbolBinding.GlobalBinding: + self.entries.append(sym) + for sym in data.get_symbols_of_type(SymbolType.DataSymbol): + if sym.binding == SymbolBinding.GlobalBinding: + self.entries.append(sym) + if data.view_type == "PE": + self.ordinal_col = 0 + self.addr_col = 1 + self.name_col = 2 + self.total_cols = 3 + + def columnCount(self, parent): + return self.total_cols + + def rowCount(self, parent): + if parent.isValid(): + return 0 + return len(self.entries) + + def data(self, index, role): + if role != Qt.DisplayRole: + return None + if index.row() >= len(self.entries): + return None + if index.column() == self.addr_col: + return "0x%x" % self.entries[index.row()].address + if index.column() == self.name_col: + return self.entries[index.row()].full_name + if index.column() == self.ordinal_col: + return str(self.entries[index.row()].ordinal) + return None + + def headerData(self, section, orientation, role): + if orientation == Qt.Vertical: + return None + if role != Qt.DisplayRole: + return None + if section == self.addr_col: + return "Address" + if section == self.name_col: + return "Name" + if section == self.ordinal_col: + return "Ordinal" + return None + + def index(self, row, col, parent): + if parent.isValid(): + return QModelIndex() + if row >= len(self.entries): + return QModelIndex() + if col >= self.total_cols: + return QModelIndex() + return self.createIndex(row, col) + + def parent(self, index): + return QModelIndex() + + def getSymbol(self, index): + if index.row() >= len(self.entries): + return None + return self.entries[index.row()] + + def sort(self, col, order): + self.beginResetModel() + if col == self.addr_col: + self.entries.sort(key = lambda sym: sym.address, reverse = order != Qt.AscendingOrder) + elif col == self.name_col: + self.entries.sort(key = lambda sym: sym.full_name, reverse = order != Qt.AscendingOrder) + elif col == self.ordinal_col: + self.entries.sort(key = lambda sym: sym.ordinal, reverse = order != Qt.AscendingOrder) + self.endResetModel() + + +class ExportsWidget(QTreeView): + def __init__(self, parent, view, data): + super(ExportsWidget, self).__init__(parent) + self.data = data + self.view = view + + self.model = GenericExportsModel(self.data) + self.setModel(self.model) + self.setRootIsDecorated(False) + self.setUniformRowHeights(True) + self.setSortingEnabled(True) + self.sortByColumn(0, Qt.AscendingOrder) + if self.model.ordinal_col is not None: + self.setColumnWidth(self.model.ordinal_col, 55) + self.setMinimumSize(QSize(100, 196)) + + self.setFont(binaryninjaui.getMonospaceFont(self)) + + self.selectionModel().currentChanged.connect(self.exportSelected) + self.doubleClicked.connect(self.exportDoubleClicked) + + def exportSelected(self, cur, prev): + sym = self.model.getSymbol(cur) + if sym is not None: + self.view.setCurrentOffset(sym.address) + + def exportDoubleClicked(self, cur): + sym = self.model.getSymbol(cur) + if sym is not None: + viewFrame = ViewFrame.viewFrameForWidget(self) + if len(self.data.get_functions_at(sym.address)) > 0: + viewFrame.navigate("Graph:" + viewFrame.getCurrentDataType(), sym.address) + else: + viewFrame.navigate("Linear:" + viewFrame.getCurrentDataType(), sym.address) diff --git a/python/examples/triage/files.py b/python/examples/triage/files.py new file mode 100644 index 00000000..333bcf5a --- /dev/null +++ b/python/examples/triage/files.py @@ -0,0 +1,140 @@ +import os +from PySide2.QtWidgets import QWidget, QTreeView, QFileSystemModel, QVBoxLayout, QMessageBox, QAbstractItemView +from PySide2.QtGui import QKeySequence +from PySide2.QtCore import QSettings +from binaryninjaui import UIActionHandler, UIAction, Menu, FileContext, ContextMenuManager +from binaryninja.settings import Settings + + +class TriageFilePicker(QWidget): + def __init__(self, context): + super(TriageFilePicker, self).__init__() + self.context = context + self.actionHandler = UIActionHandler() + self.actionHandler.setupActionHandler(self) + self.contextMenu = Menu() + self.contextMenuManager = ContextMenuManager(self) + + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + + self.model = QFileSystemModel() + self.model.setRootPath("") + self.tree = QTreeView(self) + self.tree.setModel(self.model) + self.tree.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tree.setColumnWidth(0, 500) + layout.addWidget(self.tree, 1) + + self.setLayout(layout) + + self.tree.doubleClicked.connect(self.onDoubleClick) + + recentFile = QSettings().value("triage/recentFile", os.path.expanduser("~")) + while len(recentFile) > 0: + f = self.model.index(recentFile) + if f.isValid(): + self.tree.scrollTo(f) + self.tree.setExpanded(f, True) + break + parentDir = os.path.dirname(recentFile) + if parentDir == recentFile: + break + recentFile = parentDir + + self.actionHandler.bindAction("Open Selected Files", UIAction( + lambda context: self.openSelectedFiles(), + lambda context: self.areFilesSelected())) + self.contextMenu.addAction("Open Selected Files", "Open") + + def contextMenuEvent(self, event): + self.contextMenuManager.show(self.contextMenu, self.actionHandler) + + def onDoubleClick(self, index): + self.openSelectedFiles() + + def openSelectedFiles(self): + failedToOpen = [] + files = set() + + for index in self.tree.selectionModel().selectedIndexes(): + if self.model.fileInfo(index).isFile(): + files.add(self.model.fileInfo(index).absoluteFilePath()) + + for filename in files: + QSettings().setValue("triage/recentFile", filename) + + f = FileContext.openFilename(filename) + if not f: + failedToOpen.append(filename) + continue + + f.createBinaryViews() + for data in f.getAllDataViews(): + Settings().set_string("analysis.mode", Settings().get_string("triage.analysis_mode"), data) + Settings().set_bool("triage.always_prefer", True, data) + if data.view_type != "Raw": + linearSweepMode = Settings().get_string("triage.linear_sweep") + if linearSweepMode == "none": + Settings().set_bool("analysis.linearSweep.autorun", False, data) + elif linearSweepMode == "partial": + Settings().set_bool("analysis.linearSweep.autorun", True, data) + Settings().set_bool("analysis.linearSweep.controlFlowGraph", False, data) + elif linearSweepMode == "full": + Settings().set_bool("analysis.linearSweep.autorun", True, data) + Settings().set_bool("analysis.linearSweep.controlFlowGraph", True, data) + + self.context.openFileContext(f) + + if len(failedToOpen) > 0: + QMessageBox.critical(self, "Error", "Unable to open:\n" + "\n".join(failedToOpen)) + + def areFilesSelected(self): + return self.tree.selectionModel().hasSelection() + + +def openForTriage(context): + currentContext = context.context + if currentContext is None: + return + + # Do not try to set the parent window when creating tabs, as this will create a parent relationship in + # the bindings and will cause the widget to be destructed early. The correct parent will be assigned + # when createTabForWidget is called. + fp = TriageFilePicker(currentContext) + currentContext.createTabForWidget("Open for Triage", fp) + + +Settings().register_setting("triage.analysis_mode", """ + { + "title" : "Triage Analysis Mode", + "type" : "string", + "default" : "basic", + "description" : "Controls the amount of analysis performed on functions when opening for triage.", + "enum" : ["controlFlow", "basic", "full"], + "enumDescriptions" : [ + "Only perform control flow analysis on the binary. Cross references are valid only for direct function calls.", + "Perform fast initial analysis of the binary. This mode does not analyze types or data flow through stack variables.", + "Perform full analysis of the binary." ] + } + """) + +Settings().register_setting("triage.linear_sweep", """ + { + "title" : "Triage Linear Sweep Mode", + "type" : "string", + "default" : "partial", + "description" : "Controls the level of linear sweep performed when opening for triage.", + "enum" : ["none", "partial", "full"], + "enumDescriptions" : [ + "Do not perform linear sweep of the binary.", + "Perform linear sweep on the binary, but skip the control flow graph analysis phase.", + "Perform full linear sweep on the binary." ] + } + """) + +UIAction.registerAction("Open for Triage...", QKeySequence("Ctrl+Alt+O")) +UIAction.registerAction("Open Selected Files") + +UIActionHandler.globalActions().bindAction("Open for Triage...", UIAction(openForTriage)) +Menu.mainMenu("File").addAction("Open for Triage...", "Open") diff --git a/python/examples/triage/headers.py b/python/examples/triage/headers.py index d800086c..47869134 100644 --- a/python/examples/triage/headers.py +++ b/python/examples/triage/headers.py @@ -1,6 +1,53 @@ import time from binaryninja.binaryview import StructuredDataView +import binaryninjaui +from binaryninjaui import ThemeColor, ViewFrame from PySide2.QtWidgets import QWidget, QLabel, QGridLayout +from PySide2.QtGui import QPalette + + +class ClickableLabel(QLabel): + def __init__(self, text, color, func): + super(ClickableLabel, self).__init__(text) + style = QPalette(self.palette()) + style.setColor(QPalette.WindowText, color) + self.setPalette(style) + self.setFont(binaryninjaui.getMonospaceFont(self)) + self.func = func + + def mousePressEvent(self, event): + self.func() + + +class ClickableAddressLabel(ClickableLabel): + def __init__(self, text): + super(ClickableAddressLabel, self).__init__(text, binaryninjaui.getThemeColor(ThemeColor.AddressColor), self.clickEvent) + self.address = int(text, 0) + + def clickEvent(self): + viewFrame = ViewFrame.viewFrameForWidget(self) + viewFrame.navigate("Linear:" + viewFrame.getCurrentDataType(), self.address) + + +class ClickableCodeLabel(ClickableLabel): + def __init__(self, text): + super(ClickableCodeLabel, self).__init__(text, binaryninjaui.getThemeColor(ThemeColor.CodeSymbolColor), self.clickEvent) + self.address = int(text, 0) + + def clickEvent(self): + viewFrame = ViewFrame.viewFrameForWidget(self) + viewFrame.navigate("Graph:" + viewFrame.getCurrentDataType(), self.address) + + +class GenericHeaders(object): + def __init__(self, data): + self.fields = [] + self.fields.append(("Type", data.view_type)) + if data.platform is not None: + self.fields.append(("Platform", data.platform.name)) + if data.is_valid_offset(data.entry_point): + self.fields.append(("Entry Point", "0x%x" % data.entry_point, "code")) + self.columns = 1 class PEHeaders(object): @@ -42,10 +89,10 @@ class PEHeaders(object): self.fields.append(("Timestamp", time.strftime("%c", time.localtime(int(coff.timeDateStamp))))) base = int(peopt.imageBase) - self.fields.append(("Image Base", "0x%x" % base)) + self.fields.append(("Image Base", "0x%x" % base, "ptr")) entry_point = base + int(peopt.addressOfEntryPoint) - self.fields.append(("Entry Point", "0x%x" % entry_point)) + self.fields.append(("Entry Point", "0x%x" % entry_point, "code")) section_align = int(peopt.sectionAlignment) self.fields.append(("Section Alignment", "0x%x" % section_align)) @@ -57,11 +104,11 @@ class PEHeaders(object): self.fields.append(("Checksum", "0x%.8x" % checksum)) code_base = base + int(peopt.baseOfCode) - self.fields.append(("Base of Code", "0x%x" % code_base)) + self.fields.append(("Base of Code", "0x%x" % code_base, "ptr")) if not is64bit: data_base = base + int(peopt.baseOfData) - self.fields.append(("Base of Data", "0x%x" % data_base)) + self.fields.append(("Base of Data", "0x%x" % data_base, "ptr")) code_size = int(peopt.sizeOfCode) self.fields.append(("Size of Code", "0x%x" % code_size)) @@ -126,6 +173,9 @@ class PEHeaders(object): if len(dll_char_values) > 0: self.fields.append(("DLL Characteristics", dll_char_values)) + self.columns = 3 + self.rows_per_column = 9 + class HeaderWidget(QWidget): def __init__(self, parent, header): @@ -138,18 +188,35 @@ class HeaderWidget(QWidget): for field in header.fields: name = field[0] value = field[1] - layout.addWidget(QLabel(name + ": "), row, col) + fieldType = "" + if len(field) > 2: + fieldType = field[2] + layout.addWidget(QLabel(name + ": "), row, col * 3) if isinstance(value, list): for i in range(0, len(value)): - layout.addWidget(QLabel(value[i]), row, col + 1) + if fieldType == "ptr": + label = ClickableAddressLabel(value[i]) + elif fieldType == "code": + label = ClickableCodeLabel(value[i]) + else: + label = QLabel(value[i]) + label.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(label, row, col * 3 + 1) row += 1 else: - layout.addWidget(QLabel(value), row, col + 1) + if fieldType == "ptr": + label = ClickableAddressLabel(value) + elif fieldType == "code": + label = ClickableCodeLabel(value) + else: + label = QLabel(value) + label.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(label, row, col * 3 + 1) row += 1 - if (row >= 9) and (col < 6): + if (header.columns > 1) and (row >= header.rows_per_column) and ((col + 1) < header.columns): row = 0 - col += 3 - layout.setColumnMinimumWidth(2, 20) - layout.setColumnMinimumWidth(5, 20) - layout.setColumnStretch(8, 1) + col += 1 + for col in range(1, header.columns): + layout.setColumnMinimumWidth(col * 3 - 1, 20) + layout.setColumnStretch(header.columns * 3 - 1, 1) self.setLayout(layout) diff --git a/python/examples/triage/imports.py b/python/examples/triage/imports.py index 2b2ed253..293ed239 100644 --- a/python/examples/triage/imports.py +++ b/python/examples/triage/imports.py @@ -1,7 +1,8 @@ from PySide2.QtWidgets import QTreeView -from PySide2.QtCore import Qt, QAbstractItemModel, QModelIndex +from PySide2.QtCore import Qt, QAbstractItemModel, QModelIndex, QSize from binaryninja.enums import SymbolType import binaryninjaui +from binaryninjaui import ViewFrame class GenericImportsModel(QAbstractItemModel): @@ -11,20 +12,24 @@ class GenericImportsModel(QAbstractItemModel): self.has_modules = False self.name_col = 1 self.module_col = None + self.ordinal_col = None self.total_cols = 2 for sym in data.get_symbols_of_type(SymbolType.ImportAddressSymbol): self.entries.append(sym) if str(sym.namespace) != "BNINTERNALNAMESPACE": self.has_modules = True if self.has_modules: - self.name_col = 2 + self.name_col = 3 self.module_col = 1 - self.total_cols = 3 + self.ordinal_col = 2 + self.total_cols = 4 def columnCount(self, parent): return self.total_cols def rowCount(self, parent): + if parent.isValid(): + return 0 return len(self.entries) def data(self, index, role): @@ -35,7 +40,7 @@ class GenericImportsModel(QAbstractItemModel): if index.column() == 0: return "0x%x" % self.entries[index.row()].address if index.column() == self.name_col: - name = self.entries[index.row()].name + name = self.entries[index.row()].full_name if name.endswith("@GOT"): name = name[:-len("@GOT")] elif name.endswith("@PLT"): @@ -45,6 +50,8 @@ class GenericImportsModel(QAbstractItemModel): return name if index.column() == self.module_col: return self.getNamespace(self.entries[index.row()]) + if index.column() == self.ordinal_col: + return str(self.entries[index.row()].ordinal) return None def headerData(self, section, orientation, role): @@ -58,6 +65,8 @@ class GenericImportsModel(QAbstractItemModel): return "Name" if section == self.module_col: return "Module" + if section == self.ordinal_col: + return "Ordinal" return None def index(self, row, col, parent): @@ -88,9 +97,11 @@ class GenericImportsModel(QAbstractItemModel): if col == 0: self.entries.sort(key = lambda sym: sym.address, reverse = order != Qt.AscendingOrder) elif col == self.name_col: - self.entries.sort(key = lambda sym: sym.name, reverse = order != Qt.AscendingOrder) + self.entries.sort(key = lambda sym: sym.full_name, reverse = order != Qt.AscendingOrder) elif col == self.module_col: self.entries.sort(key = lambda sym: self.getNamespace(sym), reverse = order != Qt.AscendingOrder) + elif col == self.ordinal_col: + self.entries.sort(key = lambda sym: sym.ordinal, reverse = order != Qt.AscendingOrder) self.endResetModel() @@ -106,12 +117,22 @@ class ImportsWidget(QTreeView): self.setUniformRowHeights(True) self.setSortingEnabled(True) self.sortByColumn(0, Qt.AscendingOrder) + if self.model.ordinal_col is not None: + self.setColumnWidth(self.model.ordinal_col, 55) + self.setMinimumSize(QSize(100, 196)) self.setFont(binaryninjaui.getMonospaceFont(self)) - self.clicked.connect(self.importSelected) + self.selectionModel().currentChanged.connect(self.importSelected) + self.doubleClicked.connect(self.importDoubleClicked) - def importSelected(self, index): - sym = self.model.getSymbol(index) + def importSelected(self, cur, prev): + sym = self.model.getSymbol(cur) if sym is not None: self.view.setCurrentOffset(sym.address) + + def importDoubleClicked(self, cur): + sym = self.model.getSymbol(cur) + if sym is not None: + viewFrame = ViewFrame.viewFrameForWidget(self) + viewFrame.navigate("Linear:" + viewFrame.getCurrentDataType(), sym.address) diff --git a/python/examples/triage/sections.py b/python/examples/triage/sections.py new file mode 100644 index 00000000..4a8d0f65 --- /dev/null +++ b/python/examples/triage/sections.py @@ -0,0 +1,147 @@ +# coding: utf8 + +from PySide2.QtWidgets import QWidget, QLabel, QGridLayout, QHBoxLayout +from binaryninja.enums import SectionSemantics +import binaryninjaui +from binaryninjaui import ThemeColor, ViewFrame +from . import headers + + +class SegmentsWidget(QWidget): + def __init__(self, parent, data): + super(SegmentsWidget, self).__init__(parent) + + layout = QGridLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setVerticalSpacing(1) + layout.setHorizontalSpacing(16) + + self.segments = [] + for segment in data.segments: + if segment.readable or segment.writable or segment.executable: + self.segments.append(segment) + self.segments.sort(key = lambda segment: segment.start) + + row = 0 + for segment in self.segments: + begin = "0x%x" % segment.start + end = "0x%x" % segment.end + + permissions = "" + if segment.readable: + permissions += "r" + else: + permissions += "-" + if segment.writable: + permissions += "w" + else: + permissions += "-" + if segment.executable: + permissions += "x" + else: + permissions += "-" + + rangeLayout = QHBoxLayout() + rangeLayout.setContentsMargins(0, 0, 0, 0) + beginLabel = headers.ClickableAddressLabel(begin) + dashLabel = QLabel("-") + dashLabel.setFont(binaryninjaui.getMonospaceFont(self)) + endLabel = headers.ClickableAddressLabel(end) + rangeLayout.addWidget(beginLabel) + rangeLayout.addWidget(dashLabel) + rangeLayout.addWidget(endLabel) + layout.addLayout(rangeLayout, row, 0) + + permissionsLabel = QLabel(permissions) + permissionsLabel.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(permissionsLabel, row, 1) + + row += 1 + + layout.setColumnStretch(2, 1) + self.setLayout(layout) + + +class SectionsWidget(QWidget): + def __init__(self, parent, data): + super(SectionsWidget, self).__init__(parent) + + layout = QGridLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setVerticalSpacing(1) + layout.setHorizontalSpacing(16) + + maxNameLen = 0 + for section in data.sections.values(): + if len(section.name) > maxNameLen: + maxNameLen = len(section.name) + if maxNameLen > 32: + maxNameLen = 32 + + self.sections = [] + for section in data.sections.values(): + if section.semantics != SectionSemantics.ExternalSectionSemantics: + self.sections.append(section) + self.sections.sort(key = lambda section: section.start) + + row = 0 + for section in self.sections: + name = section.name + if len(name) > maxNameLen: + name = name[:maxNameLen - 1] + "…" + + begin = "0x%x" % section.start + end = "0x%x" % section.end + typeName = section.type + + permissions = "" + if data.is_offset_readable(section.start): + permissions += "r" + else: + permissions += "-" + if data.is_offset_writable(section.start): + permissions += "w" + else: + permissions += "-" + if data.is_offset_executable(section.start): + permissions += "x" + else: + permissions += "-" + + semantics = "" + if section.semantics == SectionSemantics.ReadOnlyCodeSectionSemantics: + semantics = "Code" + elif section.semantics == SectionSemantics.ReadOnlyDataSectionSemantics: + semantics = "Ready-only Data" + elif section.semantics == SectionSemantics.ReadWriteDataSectionSemantics: + semantics = "Writable Data" + + nameLabel = QLabel(name) + nameLabel.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(nameLabel, row, 0) + + rangeLayout = QHBoxLayout() + rangeLayout.setContentsMargins(0, 0, 0, 0) + beginLabel = headers.ClickableAddressLabel(begin) + dashLabel = QLabel("-") + dashLabel.setFont(binaryninjaui.getMonospaceFont(self)) + endLabel = headers.ClickableAddressLabel(end) + rangeLayout.addWidget(beginLabel) + rangeLayout.addWidget(dashLabel) + rangeLayout.addWidget(endLabel) + layout.addLayout(rangeLayout, row, 1) + + permissionsLabel = QLabel(permissions) + permissionsLabel.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(permissionsLabel, row, 2) + typeLabel = QLabel(typeName) + typeLabel.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(typeLabel, row, 3) + semanticsLabel = QLabel(semantics) + semanticsLabel.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(semanticsLabel, row, 4) + + row += 1 + + layout.setColumnStretch(5, 1) + self.setLayout(layout) diff --git a/python/examples/triage/view.py b/python/examples/triage/view.py index e06776ca..863ae045 100644 --- a/python/examples/triage/view.py +++ b/python/examples/triage/view.py @@ -2,11 +2,15 @@ import traceback import binaryninjaui from binaryninja.settings import Settings from binaryninja import log -from binaryninjaui import View, ViewType, UIContext -from PySide2.QtWidgets import QScrollArea, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QGroupBox +from binaryninjaui import View, ViewType, UIContext, ViewFrame +from PySide2.QtWidgets import QScrollArea, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QGroupBox, QSplitter +from PySide2.QtCore import Qt from . import headers from . import entropy from . import imports +from . import exports +from . import sections +from . import byte class TriageView(QScrollArea, View): @@ -16,13 +20,15 @@ class TriageView(QScrollArea, View): self.setupView(self) self.data = data self.currentOffset = 0 + self.byteView = None + self.fullAnalysisButton = None container = QWidget(self) layout = QVBoxLayout() entropyGroup = QGroupBox("Entropy") entropyLayout = QVBoxLayout() - entropyLayout.addWidget(entropy.EntropyWidget(entropyGroup, self.data)) + entropyLayout.addWidget(entropy.EntropyWidget(entropyGroup, self, self.data)) entropyGroup.setLayout(entropyLayout) layout.addWidget(entropyGroup) @@ -30,6 +36,8 @@ class TriageView(QScrollArea, View): try: if self.data.view_type == "PE": hdr = headers.PEHeaders(self.data) + elif self.data.view_type != "Raw": + hdr = headers.GenericHeaders(self.data) except: log.log_error(traceback.format_exc()) @@ -41,33 +49,73 @@ class TriageView(QScrollArea, View): headerGroup.setLayout(headerLayout) layout.addWidget(headerGroup) - importGroup = QGroupBox("Imports") - importLayout = QVBoxLayout() - importLayout.addWidget(imports.ImportsWidget(importGroup, self, self.data)) - importGroup.setLayout(importLayout) - layout.addWidget(importGroup) + if self.data.executable: + importExportSplitter = QSplitter(Qt.Horizontal) - button_layout = QHBoxLayout() - button_layout.addStretch(1) - self.full_analysis_button = QPushButton("Start Full Analysis") - self.full_analysis_button.clicked.connect(self.startFullAnalysis) - button_layout.addWidget(self.full_analysis_button) - layout.addLayout(button_layout) + importGroup = QGroupBox("Imports") + importLayout = QVBoxLayout() + importLayout.addWidget(imports.ImportsWidget(importGroup, self, self.data)) + importGroup.setLayout(importLayout) + importExportSplitter.addWidget(importGroup) + + exportGroup = QGroupBox("Exports") + exportLayout = QVBoxLayout() + exportLayout.addWidget(exports.ExportsWidget(exportGroup, self, self.data)) + exportGroup.setLayout(exportLayout) + importExportSplitter.addWidget(exportGroup) + + layout.addWidget(importExportSplitter) + + if self.data.view_type != "PE": + segmentsGroup = QGroupBox("Segments") + segmentsLayout = QVBoxLayout() + segmentsWidget = sections.SegmentsWidget(segmentsGroup, self.data) + segmentsLayout.addWidget(segmentsWidget) + segmentsGroup.setLayout(segmentsLayout) + layout.addWidget(segmentsGroup) + if len(segmentsWidget.segments) == 0: + segmentsGroup.hide() + + sectionsGroup = QGroupBox("Sections") + sectionsLayout = QVBoxLayout() + sectionsWidget = sections.SectionsWidget(sectionsGroup, self.data) + sectionsLayout.addWidget(sectionsWidget) + sectionsGroup.setLayout(sectionsLayout) + layout.addWidget(sectionsGroup) + if len(sectionsWidget.sections) == 0: + sectionsGroup.hide() + + buttonLayout = QHBoxLayout() + buttonLayout.addStretch(1) + self.fullAnalysisButton = QPushButton("Start Full Analysis") + self.fullAnalysisButton.clicked.connect(self.startFullAnalysis) + buttonLayout.addWidget(self.fullAnalysisButton) + layout.addLayout(buttonLayout) + layout.addStretch(1) + else: + self.byteView = byte.ByteView(self, self.data) + layout.addWidget(self.byteView, 1) - layout.addStretch(1) container.setLayout(layout) self.setWidgetResizable(True) self.setWidget(container) - if Settings().get_string("analysis.mode", data) == "full": - self.full_analysis_button.hide() + if self.fullAnalysisButton is not None and Settings().get_string("analysis.mode", data) == "full": + self.fullAnalysisButton.hide() def getData(self): return self.data def getCurrentOffset(self): + if self.byteView is not None: + return self.byteView.getCurrentOffset() return self.currentOffset + def getSelectionOffsets(self): + if self.byteView is not None: + return self.byteView.getSelectionOffsets() + return (self.currentOffset, self.currentOffset) + def setCurrentOffset(self, offset): self.currentOffset = offset UIContext.updateStatus(True) @@ -76,6 +124,8 @@ class TriageView(QScrollArea, View): return binaryninjaui.getMonospaceFont(self) def navigate(self, addr): + if self.byteView is not None: + return self.navigate(addr) return False def startFullAnalysis(self): @@ -84,7 +134,34 @@ class TriageView(QScrollArea, View): if f.analysis_skipped: f.reanalyze() self.data.update_analysis() - self.full_analysis_button.hide() + self.fullAnalysisButton.hide() + + def navigateToFileOffset(self, offset): + if self.byteView is None: + addr = self.data.get_address_for_data_offset(offset) + view_frame = ViewFrame.viewFrameForWidget(self) + if view_frame is None: + return + if addr is None: + view_frame.navigate("Hex:Raw", offset) + else: + view_frame.navigate("Linear:" + view_frame.getCurrentDataType(), addr) + else: + if self.data == self.data.file.raw: + addr = offset + else: + addr = self.data.get_address_for_data_offset(offset) + if addr is None: + view_frame = ViewFrame.viewFrameForWidget(self) + if view_frame is not None: + view_frame.navigate("Hex:Raw", offset) + else: + self.byteView.navigate(addr) + self.byteView.setFocus(Qt.OtherFocusReason) + + def focusInEvent(self, event): + if self.byteView is not None: + self.byteView.setFocus(Qt.OtherFocusReason) class TriageViewType(ViewType): @@ -93,16 +170,22 @@ class TriageViewType(ViewType): def getPriority(self, data, filename): is_full = Settings().get_string("analysis.mode", data) == "full" - always_prefer = Settings().get_bool("ui.always_prefer_triage", data) + always_prefer = Settings().get_bool("triage.always_prefer", data) + prefer_for_raw = Settings().get_bool("triage.prefer_for_raw", data) if data.executable and (always_prefer or not is_full): return 100 - return 25 + if len(data) > 0: + if always_prefer or data.executable or prefer_for_raw: + return 25 + return 1 + return 0 def create(self, data, view_frame): return TriageView(view_frame, data) -Settings().register_setting("ui.always_prefer_triage", """ +Settings().register_group("triage", "Triage") +Settings().register_setting("triage.always_prefer", """ { "title" : "Always Prefer Triage Summary View", "type" : "boolean", @@ -111,4 +194,13 @@ Settings().register_setting("ui.always_prefer_triage", """ } """) +Settings().register_setting("triage.prefer_for_raw", """ + { + "title" : "Prefer Triage Summary View for Raw Files", + "type" : "boolean", + "default" : true, + "description" : "Prefer opening raw files in Triage Summary view." + } + """) + ViewType.registerViewType(TriageViewType()) |
