#!/usr/bin/python # coding: utf-8 # # HeapViewer - by @danigargu # import idaapi from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5.QtCore import Qt from collections import OrderedDict from cgi import escape as html_encode from heap_viewer.misc import * from heap_viewer.widgets.custom import CustomWidget, TTable from heap_viewer import io_file from heap_viewer import config # ----------------------------------------------------------------------- class MagicWidget(CustomWidget): def __init__(self, parent=None): super(MagicWidget, self).__init__(parent) self._create_gui() def _create_gui(self): self.cb_magic = QtWidgets.QComboBox() self.cb_magic.setFixedWidth(200) self.cb_magic.addItem('Unlink merge info', 0) self.cb_magic.addItem('Find fake fast', 0) self.cb_magic.addItem('House of force helper', 0) self.cb_magic.addItem('Useful libc offsets') self.cb_magic.addItem('Calc chunk size') self.cb_magic.addItem('IO_FILE structs') self.cb_magic.addItem('Freeable chunk') self.cb_magic.currentIndexChanged[int].connect(self.cb_magic_changed) self.stacked_magic = QtWidgets.QStackedWidget() self.unlink_widget = UnlinkWidget(self) self.fakefast_widget = FakefastWidget(self) self.house_of_force_widget = HouseOfForceWidget(self) self.libc_offsets_widget = LibcOffsetsWidget(self) self.req2size_widget = Req2sizeWidget(self) self.io_file_widget = IOFileWidget(self) self.freeable_widget = FreeableWidget(self) self.stacked_magic.addWidget(self.unlink_widget) self.stacked_magic.addWidget(self.fakefast_widget) self.stacked_magic.addWidget(self.house_of_force_widget) self.stacked_magic.addWidget(self.libc_offsets_widget) self.stacked_magic.addWidget(self.req2size_widget) self.stacked_magic.addWidget(self.io_file_widget) self.stacked_magic.addWidget(self.freeable_widget) hbox_magic = QtWidgets.QHBoxLayout() hbox_magic.addWidget(QtWidgets.QLabel('Select util')) hbox_magic.addWidget(self.cb_magic) hbox_magic.addStretch(1) self.vbox_magic = QtWidgets.QVBoxLayout() self.vbox_magic.addLayout(hbox_magic) self.vbox_magic.addWidget(self.stacked_magic) self.setLayout(self.vbox_magic) def populate_libc_offsets(self): self.libc_offsets_widget.populate_table() def cb_magic_changed(self, idx): self.stacked_magic.setCurrentIndex(idx) # ----------------------------------------------------------------------- class UnlinkWidget(CustomWidget): def __init__(self, parent=None): super(UnlinkWidget, self).__init__(parent) self._create_gui() def _create_gui(self): self.t_unlink_addr = QtWidgets.QLineEdit() self.t_unlink_addr.setFixedWidth(150) self.t_unlink_info = QtWidgets.QTextEdit() self.t_unlink_info.setFixedHeight(400) self.t_unlink_info.setReadOnly(True) self.btn_check_unlink = QtWidgets.QPushButton("Check") self.btn_check_unlink.clicked.connect(self.check_unlink_on_click) hbox_unlink = QtWidgets.QHBoxLayout() hbox_unlink.addWidget(QtWidgets.QLabel('Chunk ptr unlink')) hbox_unlink.addWidget(self.t_unlink_addr) hbox_unlink.addWidget(self.btn_check_unlink) hbox_unlink.addStretch(1) vbox_unlink = QtWidgets.QVBoxLayout() vbox_unlink.addLayout(hbox_unlink) vbox_unlink.addWidget(self.t_unlink_info) vbox_unlink.addStretch(1) vbox_unlink.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox_unlink) def check_unlink_on_click(self): info_template = ''' <style> td {padding-right: 30px;} table {font-size: 12px;} body {width: 100%%;} #True {color: green} #False {color: red} </style> <table> <tr> <td>Unlinkable</td> <td><b id="%s">%s</b></td> </tr> <tr> <td>P</td> <td>0x%x</td> </tr> <tr> <td>chunk_size</td> <td>0x%x</td> </tr> <tr> <td>next->prev_size</td> <td>0x%x</td> </tr> <tr> <td>FD</td> <td>0x%x</td> </tr> <tr> <td>BK</td> <td>0x%x</td> </tr> <tr> <td>FD->bk</td> <td>0x%x</td> </tr> <tr> <td>BK->fd</td> <td>0x%x</td> </tr> </table> <p><b>Result of unlink</b></p>\n <table> <tr> <td>FD->bk (*0x%x)</td> <td>0x%x</td> </tr> <tr> <td>BK->fd (*0x%x)</td> <td><b>0x%x<b></td> </tr> </table> ''' try: p = int(self.t_unlink_addr.text(), 16) chunk = self.heap.get_chunk(p) fd_chunk = self.heap.get_chunk(chunk.fd) bk_chunk = self.heap.get_chunk(chunk.bk) next_chunk = self.heap.next_chunk(p) fd_offset = self.heap.chunk_member_offset('fd') bk_offset = self.heap.chunk_member_offset('bk') """ Unlink if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P); chunksize(P) != prev_size(next_chunk(P)) https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30 """ unlinkable = (fd_chunk.bk == p and bk_chunk.fd == p) and \ (chunk.norm_size == next_chunk.prev_size) unlinkable_s = str(unlinkable) unlink_info = info_template % (unlinkable_s, unlinkable_s, p, chunk.norm_size, next_chunk.prev_size, chunk.bk, chunk.fd, fd_chunk.bk, bk_chunk.fd, chunk.fd+bk_offset, chunk.bk, chunk.bk+fd_offset, chunk.fd) self.t_unlink_info.clear() self.t_unlink_info.insertHtml(unlink_info) except Exception as e: idaapi.warning("ERROR: " + str(e)) # ----------------------------------------------------------------------- class HouseOfForceWidget(CustomWidget): def __init__(self, parent=None): super(HouseOfForceWidget, self).__init__(parent) self._create_gui() def _create_gui(self): self.t_house_force_addr = QtWidgets.QLineEdit() self.t_house_force_addr.setFixedWidth(150) self.t_house_force_info = QtWidgets.QTextEdit() self.t_house_force_info.setReadOnly(True) self.btn_house_force = QtWidgets.QPushButton("Calc evil size") self.btn_house_force.clicked.connect(self.house_force_on_click) hbox_house_force = QtWidgets.QHBoxLayout() hbox_house_force.addWidget(QtWidgets.QLabel("Target address")) hbox_house_force.addWidget(self.t_house_force_addr) hbox_house_force.addWidget(self.btn_house_force) hbox_house_force.addStretch(1) vbox_house_force = QtWidgets.QVBoxLayout() vbox_house_force.addLayout(hbox_house_force) vbox_house_force.addWidget(self.t_house_force_info) vbox_house_force.addStretch(1) vbox_house_force.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox_house_force) def house_force_on_click(self): arena = self.heap.get_arena(self.cur_arena) target_addr = int(self.t_house_force_addr.text(), 16) ptr_size = config.ptr_size top_size_ptr = arena.top + ptr_size top_size_val = self.heap.get_ptr(top_size_ptr) evil_size = target_addr - (ptr_size * 2) - top_size_ptr evil_size_u = evil_size & config.ptr_mask html_result = ''' <style> td {padding-right: 30px;} table {font-size: 12px;} body {width: 100%%;} </style> <table> <tr> <td><b>&top</b></td> <td>0x%x</td> </tr> <tr> <td><b>&top->size</b></td> <td>0x%x</td> </tr> <tr> <td><b>top->size</b></td> <td>0x%x</td> </tr> <tr> <td><b>Evil size unsigned</b></td> <td>0x%x</td> </tr> <tr> <td><b>Evil size signed</b></td> <td>%d</td> </tr> <tr> <td><b>Formula</b></td> <td>target_address - ptr_size * 2 - &top->size</td> </tr> </table> ''' % (arena.top, top_size_ptr, top_size_val, evil_size_u, evil_size) self.t_house_force_info.clear() self.t_house_force_info.insertHtml(html_result) # ----------------------------------------------------------------------- class LibcOffsetsWidget(CustomWidget): def __init__(self, parent=None): super(LibcOffsetsWidget, self).__init__(parent) self.libc_base = None self._create_gui() self.populate_table() def _create_gui(self): self.tbl_offsets_vars = TTable(['name','offset']) self.tbl_offsets_vars.resize_columns([150, 100]) self.tbl_offsets_vars.cellDoubleClicked.connect(self.tbl_offsets_double_clicked) self.tbl_offsets_vars.customContextMenuRequested.connect(self.context_menu) self.tbl_offsets_funcs = TTable(['name','offset']) self.tbl_offsets_funcs.resize_columns([150, 100]) self.tbl_offsets_funcs.cellDoubleClicked.connect(self.tbl_offsets_double_clicked) self.tbl_offsets_funcs.customContextMenuRequested.connect(self.context_menu) vbox_offsets_vars = QtWidgets.QVBoxLayout() vbox_offsets_vars.addWidget(QtWidgets.QLabel('Variables')) vbox_offsets_vars.addWidget(self.tbl_offsets_vars) vbox_offsets_funcs = QtWidgets.QVBoxLayout() vbox_offsets_funcs.addWidget(QtWidgets.QLabel('Functions')) vbox_offsets_funcs.addWidget(self.tbl_offsets_funcs) hbox_libc_offsets = QtWidgets.QHBoxLayout() hbox_libc_offsets.addLayout(vbox_offsets_vars) hbox_libc_offsets.addLayout(vbox_offsets_funcs) hbox_libc_offsets.setContentsMargins(0, 0, 0, 0) self.setLayout(hbox_libc_offsets) def context_menu(self, position): sender = self.sender() menu = QtWidgets.QMenu() action_copy = menu.addAction("Copy value") action_copy_row = menu.addAction("Copy row") action_jump_to = menu.addAction("Jump to address") action = menu.exec_(sender.mapToGlobal(position)) if action == action_copy: sender.copy_selected_value() elif action == action_copy_row: sender.copy_selected_row() elif action == action_jump_to: offset = int(sender.item(sender.currentRow(), 1).text(), 16) address = self.libc_base + offset idc.jumpto(address) def tbl_offsets_double_clicked(self): sender = self.sender() offset = int(sender.item(sender.currentRow(), 1).text(), 16) address = self.libc_base + offset idc.jumpto(address) def populate_table(self): self.tbl_offsets_vars.clearContents() self.tbl_offsets_funcs.clearContents() self.tbl_offsets_vars.setRowCount(0) self.tbl_offsets_funcs.setRowCount(0) self.tbl_offsets_vars.setSortingEnabled(False) self.tbl_offsets_funcs.setSortingEnabled(False) offsets = self.get_libc_offsets() variables = offsets['variables'] functions = offsets['functions'] for idx, (name, offset) in enumerate(variables.items()): self.tbl_offsets_vars.insertRow(idx) self.tbl_offsets_vars.setItem(idx, 0, QtWidgets.QTableWidgetItem(name)) self.tbl_offsets_vars.setItem(idx, 1, QtWidgets.QTableWidgetItem("0x%x" % offset)) for idx, (name, offset) in enumerate(functions.items()): self.tbl_offsets_funcs.insertRow(idx) self.tbl_offsets_funcs.setItem(idx, 0, QtWidgets.QTableWidgetItem(name)) self.tbl_offsets_funcs.setItem(idx, 1, QtWidgets.QTableWidgetItem("0x%x" % offset)) self.tbl_offsets_vars.resizeRowsToContents() self.tbl_offsets_funcs.resizeRowsToContents() self.tbl_offsets_vars.setSortingEnabled(True) self.tbl_offsets_funcs.setSortingEnabled(True) def get_libc_offsets(self): libc_symbols = { 'variables': [ 'environ', '__environ', '__free_hook', '__malloc_hook', '__realloc_hook', '_IO_list_all', '_IO_2_1_stdin_', '_IO_2_1_stdout_', '_IO_2_1_stderr_', ], 'functions': [ 'system', '__libc_system', 'execve', 'open', '__open64', 'read', 'write', '__write', '_IO_gets', 'gets', 'setcontext+0x35', ] } result = { 'variables': OrderedDict(), 'functions': OrderedDict(), } self.libc_base = get_libc_base() if not self.libc_base: return result libc_names = get_libc_names() if not libc_names: idaapi.warning("Unable to get glibc symbols") return result for s_type, symbols in libc_symbols.items(): for sym in symbols: name_expr = parse_name_expr(sym) if not name_expr: continue name, offset = name_expr addr = libc_names.get(name) if addr: addr += offset offset = addr - self.libc_base result[s_type][sym] = offset return result # ----------------------------------------------------------------------- class FakefastWidget(CustomWidget): def __init__(self, parent=None): super(FakefastWidget, self).__init__(parent) self._create_gui() def _create_gui(self): self.t_fakefast_addr = QtWidgets.QLineEdit() self.t_fakefast_addr.setFixedWidth(150) self.tbl_fakefast = TTable(['fast_id', 'fast_size', 'bytes to target', 'chunk address']) self.tbl_fakefast.customContextMenuRequested.connect(self.context_menu) self.t_fakefast_info = QtWidgets.QTextEdit() self.t_fakefast_info.setReadOnly(True) self.btn_find_fakefast = QtWidgets.QPushButton("Find") self.btn_find_fakefast.clicked.connect(self.find_fakefast_on_click) hbox_fakefast = QtWidgets.QHBoxLayout() hbox_fakefast.addWidget(QtWidgets.QLabel('Target address')) hbox_fakefast.addWidget(self.t_fakefast_addr) hbox_fakefast.addWidget(self.btn_find_fakefast) hbox_fakefast.addStretch(1) vbox_fakefast = QtWidgets.QVBoxLayout() vbox_fakefast.addLayout(hbox_fakefast) vbox_fakefast.addWidget(self.tbl_fakefast) vbox_fakefast.addStretch(1) vbox_fakefast.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox_fakefast) def context_menu(self, position): sender = self.sender() menu = QtWidgets.QMenu() copy_action = menu.addAction("Copy value") copy_row = menu.addAction("Copy row") view_chunk = menu.addAction("View chunk") jump_to = menu.addAction("Jump to chunk") chunk_addr = int(sender.item(sender.currentRow(), 3).text(), 16) action = menu.exec_(sender.mapToGlobal(position)) if action == copy_action: sender.copy_selected_value() if action == copy_row: sender.copy_selected_row() elif action == jump_to: idc.jumpto(chunk_addr) elif action == view_chunk: self.parent.parent.show_chunk_info(chunk_addr) def find_fakefast_on_click(self): start_addr = int(self.t_fakefast_addr.text(), 16) fake_chunks = self.heap.find_fakefast(start_addr) if len(fake_chunks) == 0: idaapi.info("Fakefast: 0 results") return self.tbl_fakefast.clearContents() self.tbl_fakefast.setRowCount(0) self.tbl_fakefast.setSortingEnabled(False) for idx, chunk in enumerate(fake_chunks): self.tbl_fakefast.insertRow(idx) self.tbl_fakefast.setItem(idx, 0, QtWidgets.QTableWidgetItem("%d" % chunk['fast_id'])) self.tbl_fakefast.setItem(idx, 1, QtWidgets.QTableWidgetItem("0x%x" % chunk['size'])) self.tbl_fakefast.setItem(idx, 2, QtWidgets.QTableWidgetItem("%d" % chunk['bytes_to'])) self.tbl_fakefast.setItem(idx, 3, QtWidgets.QTableWidgetItem("0x%x" % chunk['address'])) self.tbl_fakefast.resizeRowsToContents() self.tbl_fakefast.resizeColumnsToContents() self.tbl_fakefast.setSortingEnabled(True) # ----------------------------------------------------------------------- class Req2sizeWidget(CustomWidget): def __init__(self, parent=None): CustomWidget.__init__(self, parent) self._create_gui() def _create_gui(self): self.t_req2size = QtWidgets.QLineEdit() self.t_req2size.setFixedWidth(150) self.t_req2size_info = QtWidgets.QTextEdit() self.t_req2size_info.setReadOnly(True) self.btn_req2size = QtWidgets.QPushButton("Calc chunk size") self.btn_req2size.clicked.connect(self.btn_req2size_on_click) hbox_req2size = QtWidgets.QHBoxLayout() hbox_req2size.addWidget(QtWidgets.QLabel("Request size")) hbox_req2size.addWidget(self.t_req2size) hbox_req2size.addWidget(self.btn_req2size) hbox_req2size.addStretch(1) vbox_req2size = QtWidgets.QVBoxLayout() vbox_req2size.addLayout(hbox_req2size) vbox_req2size.addWidget(self.t_req2size_info) vbox_req2size.addStretch(1) vbox_req2size.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox_req2size) def btn_req2size_on_click(self): req_size = eval(self.t_req2size.text()) min_chunk_size = self.heap.min_chunk_size malloc_alignment = self.heap.malloc_alignment size = self.heap.request2size(req_size) html_result = ''' <style> td {padding-right: 20px;} body {width: 100%%;} </style> <table> <tr> <td><b>Min chunk size</b></td> <td>0x%x (%d)</td> </tr> <tr> <td><b>Malloc alignment</b></td> <td>0x%x (%d)</td> </tr> <tr> <td><b>Chunk size</b></td> <td>0x%x (%d)</td> </tr> </table> ''' % (min_chunk_size,min_chunk_size, malloc_alignment, malloc_alignment, size, size) self.t_req2size_info.clear() self.t_req2size_info.insertHtml(html_result) # ----------------------------------------------------------------------- class IOFileWidget(CustomWidget): def __init__(self, parent=None): super(IOFileWidget, self).__init__(parent) self._create_gui() self.cb_struct_changed(0) def _create_gui(self): self.t_io_file = QtWidgets.QTextEdit() self.t_io_jump_t = QtWidgets.QTextEdit() self.t_io_file.setReadOnly(True) self.t_io_jump_t.setReadOnly(True) self.cb_struct = QtWidgets.QComboBox() self.cb_struct.setFixedWidth(200) io_structs = [ '_IO_2_1_stdin_', '_IO_2_1_stdout_', '_IO_2_1_stderr_' ] for i, name in enumerate(io_structs): self.cb_struct.addItem(name, i) self.cb_struct.currentIndexChanged[int].connect(self.cb_struct_changed) self.t_struct_addr = QtWidgets.QLineEdit() self.t_struct_addr.setFixedWidth(150) self.btn_parse_struct = QtWidgets.QPushButton('Show') self.btn_parse_struct.clicked.connect(self.show_struct_on_click) hbox_io_struct = QtWidgets.QHBoxLayout() hbox_io_struct.addWidget(QtWidgets.QLabel('Struct:')) hbox_io_struct.addWidget(self.cb_struct) hbox_io_struct.addWidget(QtWidgets.QLabel('Address:')) hbox_io_struct.addWidget(self.t_struct_addr) hbox_io_struct.addWidget(self.btn_parse_struct) hbox_io_struct.addStretch(1) hbox_result = QtWidgets.QGridLayout() hbox_result = QtWidgets.QHBoxLayout() hbox_result.addWidget(self.t_io_file) hbox_result.addWidget(self.t_io_jump_t) vbox_req2size = QtWidgets.QVBoxLayout() vbox_req2size.addLayout(hbox_io_struct) vbox_req2size.addLayout(hbox_result) vbox_req2size.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox_req2size) def cb_struct_changed(self, idx): struct_name = str(self.cb_struct.currentText()) address = get_name_ea_simple(struct_name) if address != BADADDR: self.t_struct_addr.setText("0x%x" % address) self.show_struct(address, struct_name) def show_struct_on_click(self): try: address = int(self.t_struct_addr.text(), 16) self.show_struct(address, "_IO_FILE") except: idaapi.warning("ERROR: Invalid address") def html_struct_table(self, struct): offsets = get_struct_offsets(struct) struct_table = '<table>' for name, ctype in struct._fields_: value = getattr(struct, name) if ctype in [c_uint32, c_uint64]: value = "0x%x" % value if ctype is c_char: value = "0x%x" % ord(value) struct_table += ''' <tr> <td>+%02X</td> <td>%s</td> <td>%s</td> </tr> ''' % (offsets[name], name, str(value)) struct_table += '</table>' return struct_table def html_template(self, name, address): template = ''' <style> td { padding-right: 30px; } table { font-size: 12px; white-space: nowrap; overflow: hidden; } body { width: 100%%; } #hexdump { font-family:Monaco; font-size:12px; } </style> <p><b>%s</b> (0x%x)<br> ''' % (name, address) return template def show_struct(self, address, struct_name): io_file_struct = io_file.parse_structs(address) if io_file_struct is None: return self.t_io_file.clear() self.t_io_jump_t.clear() io_jump_t_addr = io_file_struct.file.vtable html_table = self.html_template(struct_name, address) html_table += self.html_struct_table(io_file_struct.file) self.t_io_file.insertHtml(html_table) html_table = self.html_template("%s->vtable" % struct_name, io_jump_t_addr) html_table += self.html_struct_table(io_file_struct.vtable) self.t_io_jump_t.insertHtml(html_table) for obj in [self.t_io_file, self.t_io_jump_t]: cursor = obj.textCursor() cursor.setPosition(0) obj.setTextCursor(cursor) # ----------------------------------------------------------------------- class FreeableWidget(CustomWidget): def __init__(self, parent=None): super(FreeableWidget, self).__init__(parent) self._create_gui() def _create_gui(self): self.t_chunk_addr = QtWidgets.QLineEdit() self.t_chunk_addr.setFixedWidth(150) self.t_freeable_info = QtWidgets.QTextEdit() self.t_freeable_info.setFixedHeight(400) self.t_freeable_info.setReadOnly(True) self.btn_freeable = QtWidgets.QPushButton("Check") self.btn_freeable.clicked.connect(self.check_freeable) hbox_freeable = QtWidgets.QHBoxLayout() hbox_freeable.addWidget(QtWidgets.QLabel("Chunk address")) hbox_freeable.addWidget(self.t_chunk_addr) hbox_freeable.addWidget(self.btn_freeable) hbox_freeable.addStretch(1) vbox = QtWidgets.QVBoxLayout() vbox.addLayout(hbox_freeable) vbox.addWidget(self.t_freeable_info) vbox.addStretch(1) vbox.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox) def check_freeable(self): cur_arena = self.cur_arena chunk_addr = eval(self.t_chunk_addr.text()) if self.heap is None: idaapi.warning("Heap not initialized") return freeable, errors = self.heap.is_freeable(chunk_addr, cur_arena) freeable_str = str(freeable) html_result = ''' <style> td {padding-right: 20px;} body {width: 100%%;} #True {color: green} #False {color: red} </style> <h3>Freaable</h3> <ul> <li> <span>0x%x: <b id="%s">%s</b></span>< /li> </ul> ''' % (chunk_addr, freeable_str, freeable_str) if len(errors) > 0: html_result += '<h3>Messages / Errors:</h3>' html_result += '<ul>' for err in errors: html_result += '<li>%s</li>' % html_encode(err) html_result += '</ul>' merge_info = self.heap.merge_info(chunk_addr, cur_arena) if merge_info is not None: html_result += ''' <h3>Merge info</h3> <ul> ''' if type(merge_info) == list: for info in merge_info: html_result += '<li>%s</li>' % html_encode(info) elif type(merge_info) == str: html_result += '<li>%s</li>' % merge_info html_result += '</ul>' self.t_freeable_info.clear() self.t_freeable_info.insertHtml(html_result) # -----------------------------------------------------------------------