├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── __init__.py ├── assets └── example.png ├── bnhelpers.py ├── bnlogger.py ├── constants.py ├── delphi_analyser.py ├── examples ├── README.md ├── assets │ ├── graph.png │ └── vmt_struct.png ├── create_vmt_structs.py ├── list_vmts.py └── vmt_visualizer.py └── plugin.json /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/**/ 3 | 4 | # Github files 5 | !/.gitignore 6 | !/.travis.yml 7 | !/.editorconfig 8 | !/CODE_OF_CONDUCT.md 9 | !/LICENSE 10 | !/README.md 11 | !/TODO.md 12 | 13 | # Plugin 14 | !/assets/*.png 15 | !/examples/*.py 16 | !/examples/README.md 17 | !/examples/assets/*.png 18 | !/*.py 19 | /generate_plugininfo.py 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 ImNotAVirus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delphi VMT Analyzer (v0.1.2) 2 | 3 | Author: **ImNotAVirus** 4 | 5 | _Find Delphi VMTs and generate their structures_ 6 | 7 | ## Description: 8 | 9 | This plugin is intended to facilitate the revese engineering of binaries written in Delphi. 10 | 11 | For that, it: 12 | 13 | - Finds the Virtual Method Table (VMT) of Delphi classes 14 | - Creates a structure for each VMT (class name, instance size, virtual methods, etc...) 15 | - Creates a tag for each VMT in order to locate them more easily 16 | 17 | **Warning**: This plugin currently only supports 32bits PE (Portable Executable). For more details on upcoming features, you can refer to [TODO.md](./TODO.md). 18 | 19 | ![example](./assets/example.png) 20 | 21 | ## Installation Instructions 22 | 23 | ### Darwin 24 | 25 | No special instructions, package manager is recommended 26 | 27 | ### Windows 28 | 29 | No special instructions, package manager is recommended 30 | 31 | ### Linux 32 | 33 | No special instructions, package manager is recommended 34 | 35 | ## Examples 36 | 37 | This repo contains different examples to show how the plugin works in scripting mode. 38 | You can view them [here](./examples). 39 | 40 | **Warning**: although DelphiNinja does not require dependencies, some examples may require dependencies. 41 | For more information, please refer to this [page](./examples/README.md). 42 | 43 | ## Minimum Version 44 | 45 | This plugin has been tested with the following minimum version of Binary Ninja: 46 | 47 | * release: N/A 48 | * dev: 2440 49 | 50 | ## License 51 | 52 | This plugin is released under a MIT license. 53 | 54 | ## Metadata Version 55 | 56 | 2 57 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # DelphiNinja 2 | 3 | ## Features 4 | 5 | - [x] Ability to scan a binary and find potential VMT addresses 6 | - [x] Get ClassName from a VMT address 7 | - [x] Get ParentVmt from a VMT address 8 | - [x] Get InstanceSize from a VMT address 9 | - [x] Get ClassMethods from a VMT address 10 | - [ ] Get ClassMembers for a DelphiVMT (inspect methods?) 11 | - [x] Create Binary Ninja structure for each VMT 12 | - [ ] Define a Binary Ninja structure for a Delphi Class 13 | - [ ] For ClassMethods, set `this` type (register) to the previously created structure 14 | - [ ] Export all Delphi Class structures to C/C++ code 15 | - [ ] Auto detect Delphi version 16 | - [ ] Load IDR's Knowledge bases for system types 17 | 18 | ## TODO 19 | 20 | - [ ] Support 64 bits binaries (`read32` -> `read64`, offsets, ...) 21 | - [ ] Support all architecture (even `raw` ~> remove hardcoded `bv.sections['CODE']`) 22 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import binaryninja 2 | from binaryninja import BackgroundTaskThread, BinaryView, PluginCommand, Tag, interaction 3 | from binaryninja.enums import MessageBoxButtonSet, MessageBoxIcon, MessageBoxButtonResult 4 | 5 | from .bnhelpers import BNHelpers 6 | from .delphi_analyser import DelphiAnalyzer, DelphiVMT 7 | 8 | 9 | class AnalyzeDelphiVmtsTask(BackgroundTaskThread): 10 | def __init__(self, bv: BinaryView, tag_type: Tag, delphi_version: int): 11 | BackgroundTaskThread.__init__(self, 'Searching for VMTs...', can_cancel=True) 12 | self._bv = bv 13 | self._tag_type = tag_type 14 | self._delphi_version = delphi_version 15 | 16 | 17 | def run(self): 18 | self._bv.begin_undo_actions() 19 | 20 | analyzer = DelphiAnalyzer(self._bv, self._delphi_version) 21 | analyzer.update_analysis_and_wait(self.analyze_callback) 22 | 23 | self._bv.commit_undo_actions() 24 | self._bv.update_analysis() 25 | 26 | 27 | def analyze_callback(self, delphi_vmt: DelphiVMT): 28 | self.progress = f'VMT found at 0x{delphi_vmt.start:08x} ({delphi_vmt.class_name})' 29 | 30 | BNHelpers.create_vmt_struct(self._bv, delphi_vmt) 31 | 32 | self._bv.create_user_data_tag( 33 | delphi_vmt.start, 34 | self._tag_type, 35 | delphi_vmt.class_name, 36 | unique=True) 37 | 38 | # Removal of false positives functions 39 | # /!\ Not really sure about that 40 | for function in self._bv.get_functions_containing(delphi_vmt.start): 41 | if function.name.startswith('sub_') or function.name == 'vmt' + delphi_vmt.class_name: 42 | self._bv.remove_user_function(function) 43 | 44 | # Same here 45 | for table_addr in delphi_vmt.table_list.keys(): 46 | for function in self._bv.get_functions_containing(table_addr): 47 | if function.name.startswith('sub_'): 48 | self._bv.remove_user_function(function) 49 | 50 | 51 | def clear_tags(bv: BinaryView, tag_type_name: str): 52 | code_section = bv.sections['CODE'] 53 | start = code_section.start 54 | end = code_section.end 55 | tags = [(x, y) for x, y in bv.data_tags if y.type.name == tag_type_name] 56 | 57 | if not tags: 58 | return 59 | 60 | result = interaction.show_message_box( 61 | 'Delete old tag?', 62 | ('DelphiNinja has detected several tags associated with VMTs. Would you like to ' 63 | 'remove these tags?\nWARNING: This will not remove associated structures.'), 64 | MessageBoxButtonSet.YesNoButtonSet, 65 | MessageBoxIcon.QuestionIcon 66 | ) 67 | 68 | if result != MessageBoxButtonResult.YesButton: 69 | return 70 | 71 | for addy, tag in tags: 72 | bv.remove_user_data_tag(addy, tag) 73 | 74 | 75 | def analyze_delphi_vmts(bv: BinaryView): 76 | type_name = 'Delphi VMTs' 77 | tt = bv.tag_types[type_name] if type_name in bv.tag_types else bv.create_tag_type(type_name, '🔍') 78 | 79 | choices = [ 80 | 'Delphi 2', 81 | 'Delphi 3', 82 | 'Delphi 4', 83 | 'Delphi 5', 84 | 'Delphi 6', 85 | 'Delphi 7', 86 | 'Delphi 2005', 87 | 'Delphi 2006', 88 | 'Delphi 2007', 89 | 'Delphi 2009', 90 | 'Delphi 2010', 91 | 'Delphi 2011', 92 | 'Delphi 2012', 93 | 'Delphi 2013', 94 | 'Delphi 2014' 95 | ] 96 | 97 | index = interaction.get_choice_input( 98 | 'Please, select the Delphi version', 99 | 'Delphi version', 100 | choices 101 | ) 102 | 103 | clear_tags(bv, type_name) 104 | 105 | t = AnalyzeDelphiVmtsTask(bv, tt, int(choices[index][7:])) 106 | t.start() 107 | 108 | 109 | if binaryninja.core_ui_enabled(): 110 | PluginCommand.register( 111 | 'DelphiNinja\\Analyze current binary', 112 | 'Search and define strutures for Delphi VMTs', 113 | analyze_delphi_vmts 114 | ) 115 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImNotAVirus/delphi_ninja/345f3355977d00dc507124b1e9a1a206f186dab3/assets/example.png -------------------------------------------------------------------------------- /bnhelpers.py: -------------------------------------------------------------------------------- 1 | from binaryninja import BinaryView, Symbol, SymbolType, types, Type 2 | 3 | from .delphi_analyser import DelphiVMT 4 | from .constants import VMTFieldTypes 5 | 6 | ## Backwards compatibility for API < 2500 7 | try: 8 | StructureType = types.StructureBuilder 9 | except AttributeError: 10 | StructureType = types.Structure 11 | 12 | 13 | class BNHelpers(object): 14 | @staticmethod 15 | def create_vmt_struct(bv: BinaryView, vmt: DelphiVMT) -> bool: 16 | if not vmt.is_valid: 17 | return False 18 | 19 | ## Backwards compatibility for API < 2500 20 | try: 21 | vmt_struct = types.StructureBuilder.create() 22 | except AttributeError: 23 | vmt_struct = types.Structure() 24 | 25 | if not BNHelpers._add_vmt_fields(bv, vmt, vmt_struct): 26 | return False 27 | 28 | if not BNHelpers._add_vmt_methods(bv, vmt, vmt_struct): 29 | return False 30 | 31 | struct_type = Type.structure_type(vmt_struct) 32 | bv.define_user_data_var(vmt.start, struct_type) 33 | # bv.define_user_type(f'vmt{vmt.class_name}', struct_type) 34 | 35 | # Create Symbole for VMT 36 | vmt_sym = Symbol(SymbolType.DataSymbol, vmt.start, f'vmt{vmt.class_name}') 37 | bv.define_user_symbol(vmt_sym) 38 | return True 39 | 40 | 41 | # Protected methods 42 | 43 | @staticmethod 44 | def _add_vmt_fields(bv: BinaryView, vmt: DelphiVMT, out_struct: StructureType) -> bool: 45 | address_size = bv.address_size 46 | field_types = VMTFieldTypes(bv.arch) 47 | vmt_offsets = vmt.vmt_offsets 48 | offsets = vmt_offsets.__dict__.items() 49 | offset_map = {y:x for x, y in offsets} 50 | 51 | for offset in range(vmt_offsets.cVmtSelfPtr, vmt_offsets.cVmtParent+1, address_size): 52 | if offset == vmt_offsets.cVmtClassName: 53 | if not BNHelpers.__create_class_name_type(bv, vmt, out_struct): 54 | return False 55 | continue 56 | 57 | name = offset_map[offset] 58 | field_type = getattr(field_types, name) 59 | out_struct.append(field_type, name) 60 | 61 | return True 62 | 63 | 64 | @staticmethod 65 | def _add_vmt_methods(bv: BinaryView, vmt: DelphiVMT, out_struct: StructureType) -> bool: 66 | address_size = bv.address_size 67 | 68 | if not vmt.seek_to_vmt_offset(vmt.vmt_offsets.cVmtParent + address_size): 69 | return False 70 | 71 | for _ in range(len(vmt.virtual_methods)): 72 | value = vmt.read32() 73 | 74 | if value == 0: 75 | continue 76 | 77 | if value not in vmt.virtual_methods: 78 | prev_offset = vmt.br_offset - address_size 79 | raise RuntimeError( 80 | f'Invalid method address detected at 0x{prev_offset:08x} ({vmt.class_name})') 81 | 82 | # Create function if not exists 83 | if bv.get_function_at(value) is None: 84 | bv.create_user_function(value) 85 | 86 | function = bv.get_function_at(value) 87 | 88 | # Set method name if not already set 89 | function_name = function.name 90 | method_name = vmt.virtual_methods[value] 91 | 92 | if function_name.startswith('sub_'): 93 | bv.define_user_symbol(Symbol( 94 | SymbolType.FunctionSymbol, 95 | value, 96 | method_name 97 | )) 98 | 99 | # Add field to structure 100 | field_type = Type.pointer( 101 | bv.arch, 102 | Type.function( 103 | function.return_type, 104 | [(Type.void() if x.type is None else x.type) for x in function.parameter_vars], 105 | function.calling_convention 106 | ) 107 | ) 108 | 109 | field_name = method_name.split('.')[-1] 110 | out_struct.append(field_type, field_name) 111 | 112 | return True 113 | 114 | 115 | # Private methods 116 | 117 | @staticmethod 118 | def __create_class_name_type(bv: BinaryView, vmt: DelphiVMT, out_struct: StructureType) -> bool: 119 | vmt_offsets = vmt.vmt_offsets 120 | 121 | if not vmt.seek_to_vmt_offset(vmt_offsets.cVmtClassName): 122 | return False 123 | 124 | class_name_ptr = vmt.read32() 125 | 126 | if class_name_ptr is None: 127 | return False 128 | 129 | if not vmt.seek_to_code(class_name_ptr): 130 | return False 131 | 132 | class_name_len = vmt.read8() 133 | 134 | if class_name_len is None: 135 | return False 136 | 137 | ## Backwards compatibility for API < 2500 138 | try: 139 | class_name_struct = types.StructureBuilder.create() 140 | except AttributeError: 141 | class_name_struct = types.Structure() 142 | 143 | class_name_struct.append(Type.int(1, False), 'length') 144 | class_name_struct.append(Type.array(Type.char(), class_name_len), 'value') 145 | struct_type = Type.structure_type(class_name_struct) 146 | 147 | bv.define_user_data_var(class_name_ptr, struct_type) 148 | out_struct.append(Type.pointer(bv.arch, struct_type), 'cVmtClassName') 149 | 150 | # Create Symbole for class name 151 | class_name_sym = Symbol(SymbolType.DataSymbol, class_name_ptr, f'{vmt.class_name}Name') 152 | bv.define_user_symbol(class_name_sym) 153 | 154 | return True 155 | -------------------------------------------------------------------------------- /bnlogger.py: -------------------------------------------------------------------------------- 1 | from binaryninja import log, LogLevel 2 | from datetime import datetime 3 | from enum import IntEnum 4 | 5 | 6 | class BNLogger(object): 7 | @staticmethod 8 | def init_console(min_level=LogLevel.InfoLog): 9 | log.log_to_stdout(LogLevel.InfoLog) 10 | 11 | 12 | @staticmethod 13 | def log(msg: str, level=LogLevel.InfoLog): 14 | # FIXME: Duplicate logs when level > LogLevel.InfoLog (stdout + stderr) 15 | now = datetime.now() 16 | real_msg = f' [{now.strftime("%H:%M:%S.%f")[:-3]}] [{level.name}] {msg}' 17 | log.log(level, real_msg) 18 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | from binaryninja import Architecture, Type 2 | from enum import IntEnum 3 | 4 | 5 | class VMTOffsets(object): 6 | def __init__(self, delphi_version: int): 7 | if delphi_version == 2: 8 | self.cVmtSelfPtr = -0x34 9 | self.cVmtInitTable = -0x30 10 | self.cVmtTypeInfo = -0x2C 11 | self.cVmtFieldTable = -0x28 12 | self.cVmtMethodTable = -0x24 13 | self.cVmtDynamicTable = -0x20 14 | self.cVmtClassName = -0x1C 15 | self.cVmtInstanceSize = -0x18 16 | self.cVmtParent = -0x14 17 | self.cVmtDefaultHandler = -0x10 18 | self.cVmtNewInstance = -0xC 19 | self.cVmtFreeInstance = -8 20 | self.cVmtDestroy = -4 21 | elif delphi_version == 3: 22 | self.cVmtSelfPtr = -0x40 23 | self.cVmtIntfTable = -0x3C 24 | self.cVmtAutoTable = -0x38 25 | self.cVmtInitTable = -0x34 26 | self.cVmtTypeInfo = -0x30 27 | self.cVmtFieldTable = -0x2C 28 | self.cVmtMethodTable = -0x28 29 | self.cVmtDynamicTable = -0x24 30 | self.cVmtClassName = -0x20 31 | self.cVmtInstanceSize = -0x1C 32 | self.cVmtParent = -0x18 33 | self.cVmtSafeCallException = -0x14 34 | self.cVmtDefaultHandler = -0x10 35 | self.cVmtNewInstance = -0xC 36 | self.cVmtFreeInstance = -8 37 | self.cVmtDestroy = -4 38 | elif delphi_version in [4, 5, 6, 7, 2005, 2006, 2007]: 39 | self.cVmtSelfPtr = -0x4C 40 | self.cVmtIntfTable = -0x48 41 | self.cVmtAutoTable = -0x44 42 | self.cVmtInitTable = -0x40 43 | self.cVmtTypeInfo = -0x3C 44 | self.cVmtFieldTable = -0x38 45 | self.cVmtMethodTable = -0x34 46 | self.cVmtDynamicTable = -0x30 47 | self.cVmtClassName = -0x2C 48 | self.cVmtInstanceSize = -0x28 49 | self.cVmtParent = -0x24 50 | self.cVmtSafeCallException = -0x20 51 | self.cVmtAfterConstruction = -0x1C 52 | self.cVmtBeforeDestruction = -0x18 53 | self.cVmtDispatch = -0x14 54 | self.cVmtDefaultHandler = -0x10 55 | self.cVmtNewInstance = -0xC 56 | self.cVmtFreeInstance = -8 57 | self.cVmtDestroy = -4 58 | elif delphi_version in [2009, 2010]: 59 | self.cVmtSelfPtr = -0x58 60 | self.cVmtIntfTable = -0x54 61 | self.cVmtAutoTable = -0x50 62 | self.cVmtInitTable = -0x4C 63 | self.cVmtTypeInfo = -0x48 64 | self.cVmtFieldTable = -0x44 65 | self.cVmtMethodTable = -0x40 66 | self.cVmtDynamicTable = -0x3C 67 | self.cVmtClassName = -0x38 68 | self.cVmtInstanceSize = -0x34 69 | self.cVmtParent = -0x30 70 | self.cVmtEquals = -0x2C 71 | self.cVmtGetHashCode = -0x28 72 | self.cVmtToString = -0x24 73 | self.cVmtSafeCallException = -0x20 74 | self.cVmtAfterConstruction = -0x1C 75 | self.cVmtBeforeDestruction = -0x18 76 | self.cVmtDispatch = -0x14 77 | self.cVmtDefaultHandler = -0x10 78 | self.cVmtNewInstance = -0xC 79 | self.cVmtFreeInstance = -8 80 | self.cVmtDestroy = -4 81 | elif delphi_version in [2011, 2012, 2013, 2014]: 82 | self.cVmtSelfPtr = -0x58 83 | self.cVmtIntfTable = -0x54 84 | self.cVmtAutoTable = -0x50 85 | self.cVmtInitTable = -0x4C 86 | self.cVmtTypeInfo = -0x48 87 | self.cVmtFieldTable = -0x44 88 | self.cVmtMethodTable = -0x40 89 | self.cVmtDynamicTable = -0x3C 90 | self.cVmtClassName = -0x38 91 | self.cVmtInstanceSize = -0x34 92 | self.cVmtParent = -0x30 93 | self.cVmtEquals = -0x2C 94 | self.cVmtGetHashCode = -0x28 95 | self.cVmtToString = -0x24 96 | self.cVmtSafeCallException = -0x20 97 | self.cVmtAfterConstruction = -0x1C 98 | self.cVmtBeforeDestruction = -0x18 99 | self.cVmtDispatch = -0x14 100 | self.cVmtDefaultHandler = -0x10 101 | self.cVmtNewInstance = -0xC 102 | self.cVmtFreeInstance = -8 103 | self.cVmtDestroy = -4 104 | # self.cVmtQueryInterface = 0 105 | # self.cVmtAddRef = 4 106 | # self.cVmtRelease = 8 107 | # self.cVmtCreateObject = 0xC 108 | else: 109 | raise RuntimeError(f'Unsuported Delphi version {delphi_version}') 110 | 111 | 112 | class VMTFieldTypes(object): 113 | def __init__(self, arch: Architecture): 114 | self.cVmtSelfPtr = Type.pointer(arch, Type.void()) 115 | self.cVmtIntfTable = Type.pointer(arch, Type.void()) 116 | self.cVmtAutoTable = Type.pointer(arch, Type.void()) 117 | self.cVmtInitTable = Type.pointer(arch, Type.void()) 118 | self.cVmtTypeInfo = Type.pointer(arch, Type.void()) 119 | self.cVmtFieldTable = Type.pointer(arch, Type.void()) 120 | self.cVmtMethodTable = Type.pointer(arch, Type.void()) 121 | self.cVmtDynamicTable = Type.pointer(arch, Type.void()) 122 | self.cVmtClassName = Type.pointer(arch, Type.void()) 123 | self.cVmtInstanceSize = Type.int(4) 124 | self.cVmtParent = Type.pointer(arch, Type.void()) 125 | -------------------------------------------------------------------------------- /delphi_analyser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import copy 4 | from binaryninja import BinaryReader, BinaryView, LogLevel 5 | from typing import Callable, List, Mapping, Union 6 | 7 | from .constants import VMTOffsets 8 | from .bnlogger import BNLogger 9 | 10 | 11 | MATCH_CLASS_NAME = re.compile(rb'^[\w.:]+$') 12 | 13 | 14 | class DelphiVMT(object): 15 | ''' 16 | TODO: Doc 17 | ''' 18 | 19 | def __init__(self, bv: BinaryView, delphi_version: int, address: int): 20 | # 64 bits is currently not supported 21 | address_size = bv.arch.address_size 22 | assert address_size == 4 23 | 24 | self._vmt_address = address 25 | self._is_valid = False 26 | self._bv = bv 27 | self._br = BinaryReader(bv) 28 | self._code_section = bv.sections['CODE'] 29 | self._vmt_offsets = VMTOffsets(delphi_version) 30 | self._class_name = '' 31 | self._instance_size = 0 32 | self._parent_vmt = 0 33 | self._table_list: Mapping[int, str] = {} 34 | self._virtual_methods: Mapping[int, str] = {} 35 | 36 | if not self._check_self_ptr(): 37 | return 38 | 39 | if not self._resolve_name(): 40 | return 41 | 42 | if not self._resolve_instance_size(): 43 | return 44 | 45 | if not self._resolve_parent_vmt(): 46 | return 47 | 48 | if not self._resolve_table_list(): 49 | return 50 | 51 | if not self._resolve_virtual_methods(): 52 | return 53 | 54 | self._is_valid = True 55 | 56 | 57 | def __repr__(self): 58 | return str(self) 59 | 60 | 61 | def __str__(self): 62 | if not self._is_valid: 63 | return f'' 64 | 65 | return (f'<{self._class_name} start=0x{self.start:08X} ' 66 | f'instance_size=0x{self._instance_size:X}>') 67 | 68 | 69 | ## Properties 70 | 71 | @property 72 | def vmt_address(self) -> int: 73 | return self._vmt_address 74 | 75 | @property 76 | def is_valid(self) -> bool: 77 | return self._is_valid 78 | 79 | @property 80 | def class_name(self) -> str: 81 | return self._class_name 82 | 83 | @property 84 | def instance_size(self) -> int: 85 | return self._instance_size 86 | 87 | @property 88 | def parent_vmt(self) -> int: 89 | return self._parent_vmt 90 | 91 | @property 92 | def table_list(self) -> Mapping[int, str]: 93 | return self._table_list 94 | 95 | @property 96 | def virtual_methods(self) -> Mapping[int, str]: 97 | return self._virtual_methods 98 | 99 | @property 100 | def vmt_offsets(self) -> VMTOffsets: 101 | return copy.copy(self._vmt_offsets) 102 | 103 | @property 104 | def start(self) -> int: 105 | return self._vmt_address + self._vmt_offsets.cVmtSelfPtr 106 | 107 | @property 108 | def size(self) -> int: 109 | end = 0 # ???? 110 | return end - self.start 111 | 112 | @property 113 | def br_offset(self) -> int: 114 | return self._br.offset 115 | 116 | 117 | ## Public API 118 | 119 | def seek_to_code(self, address: int) -> bool: 120 | if not self._is_valid_code_addr(address): 121 | return False 122 | 123 | self._br.seek(address) 124 | return True 125 | 126 | 127 | def seek_to_code_offset(self, offset: int) -> bool: 128 | if not self._is_valid_code_addr(self._code_section.start + offset): 129 | return False 130 | 131 | self._br.seek(self._code_section.start + offset) 132 | return True 133 | 134 | 135 | def seek_to_vmt_offset(self, offset: int) -> bool: 136 | if not self._is_valid_code_addr(self._vmt_address + offset): 137 | return False 138 | 139 | self._br.seek(self._vmt_address + offset) 140 | return True 141 | 142 | 143 | def read8(self) -> Union[None, int]: 144 | return self._br.read8() 145 | 146 | 147 | def read32(self) -> Union[None, int]: 148 | return self._br.read32() 149 | 150 | 151 | ## Protected methods 152 | 153 | def _check_self_ptr(self) -> bool: 154 | if not self.seek_to_vmt_offset(self._vmt_offsets.cVmtSelfPtr): 155 | return False 156 | 157 | self_ptr = self._br.read32() 158 | return self_ptr == self._vmt_address 159 | 160 | 161 | def _resolve_name(self) -> bool: 162 | class_name_addr = self._get_class_name_addr() 163 | 164 | if class_name_addr is None: 165 | return False 166 | 167 | self._br.seek(class_name_addr) 168 | name_len = self._br.read8() 169 | 170 | if name_len == 0: 171 | BNLogger.log( 172 | f'Care, VMT without name (len: 0) detected at 0x{self._vmt_address:08X}', 173 | LogLevel.WarningLog 174 | ) 175 | 176 | class_name = self._br.read(name_len) 177 | 178 | if MATCH_CLASS_NAME.match(class_name) is None: 179 | return False 180 | 181 | self._class_name = class_name.decode() 182 | return True 183 | 184 | 185 | def _resolve_instance_size(self) -> bool: 186 | if not self.seek_to_vmt_offset(self._vmt_offsets.cVmtInstanceSize): 187 | return False 188 | 189 | self._instance_size = self._br.read32() 190 | return True 191 | 192 | 193 | def _resolve_parent_vmt(self) -> bool: 194 | if not self.seek_to_vmt_offset(self._vmt_offsets.cVmtParent): 195 | return False 196 | 197 | self._parent_vmt = self._br.read32() 198 | return True 199 | 200 | 201 | def _resolve_virtual_methods(self) -> bool: 202 | class_name_addr = self._get_class_name_addr() 203 | 204 | if class_name_addr is None: 205 | return False 206 | 207 | address_size = self._bv.address_size 208 | offsets = self.vmt_offsets.__dict__.items() 209 | offset_map = {y:x for x, y in offsets} 210 | tables_addr = self._table_list.keys() 211 | 212 | if not self.seek_to_vmt_offset(self._vmt_offsets.cVmtParent + address_size): 213 | return False 214 | 215 | while self._br.offset < class_name_addr and self._br.offset not in tables_addr: 216 | field_value = self._br.read32() 217 | 218 | if field_value == 0: 219 | continue 220 | 221 | if not self._is_valid_code_addr(field_value): 222 | prev_offset = self._br.offset - address_size 223 | raise RuntimeError(f'Invalid code address deteted at 0x{prev_offset:08X} ' 224 | '({self.class_name})\n If you think it\'s a bug, please open an issue on ' 225 | 'Github with the used binary or the full VMT (fields + VMT) as an attachment') 226 | 227 | field_offset = self._br.offset - self._vmt_address - address_size 228 | 229 | if field_offset in offset_map: 230 | # Remove `cVmt` prefix 231 | method_name = f'{self.class_name}.{offset_map[field_offset][4:]}' 232 | else: 233 | method_name = f'{self.class_name}.sub_{field_value:x}' 234 | 235 | self._virtual_methods[field_value] = method_name 236 | 237 | return True 238 | 239 | 240 | def _resolve_table_list(self) -> bool: 241 | if not self.seek_to_vmt_offset(self.vmt_offsets.cVmtIntfTable): 242 | return False 243 | 244 | offsets = self._vmt_offsets.__dict__.items() 245 | offset_map = {y:x[4:] for x, y in offsets} 246 | 247 | stop_at = self._vmt_address + self._vmt_offsets.cVmtClassName 248 | 249 | while self._br.offset != stop_at: 250 | prev_br_offset = self._br.offset 251 | address = self._br.read32() 252 | 253 | if address < 1: 254 | continue 255 | 256 | if not self._is_valid_code_addr(address): 257 | raise RuntimeError('Invalid table address detected') 258 | 259 | self._table_list[address] = offset_map[prev_br_offset - self._vmt_address] 260 | 261 | return True 262 | 263 | 264 | def _is_valid_code_addr(self, addy: int, allow_null=False) -> bool: 265 | if addy == 0: 266 | return allow_null 267 | return addy >= self._code_section.start and addy < self._code_section.end 268 | 269 | 270 | def _get_class_name_addr(self) -> Union[None, int]: 271 | if not self.seek_to_vmt_offset(self._vmt_offsets.cVmtClassName): 272 | return None 273 | 274 | class_name_addr = self._br.read32() 275 | 276 | if not self._is_valid_code_addr(class_name_addr): 277 | return None 278 | 279 | return class_name_addr 280 | 281 | 282 | class DelphiAnalyzer(object): 283 | ''' 284 | TODO: Doc 285 | ''' 286 | 287 | def __init__(self, bv: BinaryView, delphi_version: int): 288 | self._vmt_list: List[DelphiVMT] = [] 289 | self._bv = bv 290 | self._br = BinaryReader(bv) 291 | self._code_section = bv.sections['CODE'] 292 | self._delphi_version = delphi_version 293 | self._vmt_offsets = VMTOffsets(delphi_version) 294 | 295 | 296 | ## Properties 297 | 298 | @property 299 | def delphi_version(self) -> int: 300 | return self._delphi_version 301 | 302 | @property 303 | def vmt_list(self) -> List[DelphiVMT]: 304 | return self._vmt_list 305 | 306 | 307 | ## Public API 308 | 309 | def update_analysis_and_wait(self, callback: Callable[[DelphiVMT], None] = None): 310 | self._vmt_list = [] 311 | self._seek_to_code_offset(0) 312 | 313 | while True: 314 | addy = self._get_possible_vmt() 315 | 316 | if not addy: 317 | break 318 | 319 | delphi_vmt = DelphiVMT(self._bv, self._delphi_version, addy) 320 | 321 | if not delphi_vmt.is_valid: 322 | continue 323 | 324 | self._vmt_list.append(delphi_vmt) 325 | 326 | if callback is not None: 327 | callback(delphi_vmt) 328 | 329 | 330 | ## Protected methods 331 | 332 | def _seek_to_code_offset(self, offset: int): 333 | self._br.seek(self._code_section.start + offset) 334 | 335 | 336 | def _get_possible_vmt(self) -> int: 337 | address_size = self._bv.arch.address_size 338 | 339 | if address_size != 4: 340 | raise RuntimeError('Only 32 bits architectures are currently supported') 341 | 342 | while self._br.offset <= self._code_section.end - address_size: 343 | begin = self._br.offset 344 | class_vmt = self._br.read32() 345 | if begin == class_vmt + self._vmt_offsets.cVmtSelfPtr: 346 | return class_vmt 347 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## list_vmts.py 4 | 5 | **Description**: 6 | 7 | A simple script displaying all VMTs found for a Delphi binary. 8 | It displays, for each VMT, its start address as well as the size of the instances of its class. 9 | 10 | **Dependencies**: 11 | 12 | - N/A 13 | 14 | **Usage**: 15 | 16 | > python3 examples/list_vmts.py 17 | Usage: examples/list_vmts.py 18 | 19 | > python3 examples/list_vmts.py binary.exe 7 20 | [13:37:42.724] [InfoLog] File loaded 21 | [13:37:42.724] [InfoLog] ----------------------------- 22 | [13:37:42.725] [InfoLog] Searching for VMT... 23 | [13:37:42.730] [InfoLog] 24 | [13:37:42.763] [InfoLog] 25 | [13:37:42.764] [InfoLog] 26 | [13:37:42.765] [InfoLog] 27 | [13:37:42.765] [InfoLog] 28 | [13:37:42.766] [InfoLog] 29 | [13:37:42.766] [InfoLog] 30 | [13:37:42.767] [InfoLog] 31 | [13:37:42.767] [InfoLog] 32 | [13:37:42.768] [InfoLog] 33 | [13:37:42.775] [InfoLog] 34 | [13:37:42.781] [InfoLog] 35 | [13:37:42.782] [InfoLog] 36 | [13:37:42.786] [InfoLog] 37 | [13:37:42.812] [InfoLog] 38 | [13:37:42.820] [InfoLog] 39 | [13:37:42.822] [InfoLog] 40 | [13:37:42.824] [InfoLog] 41 | [13:37:42.842] [InfoLog] 42 | [13:37:42.843] [InfoLog] 43 | [13:37:42.844] [InfoLog] 44 | [13:37:42.845] [InfoLog] 45 | [13:37:42.846] [InfoLog] 46 | [13:37:42.847] [InfoLog] 47 | [13:37:42.848] [InfoLog] 48 | [13:37:42.849] [InfoLog] 49 | [13:37:42.851] [InfoLog] 50 | [13:37:42.868] [InfoLog] 51 | 52 | ## vmt_visualizer.py 53 | 54 | **Description**: 55 | 56 | A script to graphically represent the inheritance of all classes for a Delphi binary. 57 | 58 | **Dependencies**: 59 | 60 | - pip install graphviz 61 | - Graphviz binaries ([https://graphviz.org/download/](https://graphviz.org/download/#executable-packages)) 62 | 63 | **Usage**: 64 | 65 | > python3 examples/vmt_visualizer.py 66 | Usage: examples/vmt_visualizer.py 67 | 68 | > python3 examples/vmt_visualizer.py binary.exe 7 69 | 70 | ![graph](./assets/graph.png) 71 | 72 | ## create_vmt_struct.py 73 | 74 | **Description**: 75 | 76 | This script finds the VMTs of a binary, creates the corresponding structures and saves them in a Binary Ninja (`.bndb`) database. 77 | It's the equivalent of the `DelphiNinja\Analyze current binary` command in graphical mode. 78 | 79 | **Dependencies**: 80 | 81 | - N/A 82 | 83 | **Usage**: 84 | 85 | > python3 examples/create_vmt_structs.py 86 | Usage: examples/create_vmt_structs.py 87 | 88 | > python3 examples/create_vmt_structs.py binary.exe 7 89 | [...] 90 | [13:37:42.089] [InfoLog] Saving database: `binary.exe.bndb`... 91 | 92 | ![graph](./assets/vmt_struct.png) 93 | -------------------------------------------------------------------------------- /examples/assets/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImNotAVirus/delphi_ninja/345f3355977d00dc507124b1e9a1a206f186dab3/examples/assets/graph.png -------------------------------------------------------------------------------- /examples/assets/vmt_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImNotAVirus/delphi_ninja/345f3355977d00dc507124b1e9a1a206f186dab3/examples/assets/vmt_struct.png -------------------------------------------------------------------------------- /examples/create_vmt_structs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from binaryninja import BinaryView, BinaryViewType 3 | 4 | import importlib 5 | import sys 6 | from os import path 7 | 8 | # from delphi_ninja.delphi_analyser import DelphiAnalyzer, DelphiVMT 9 | # from delphi_ninja.bnlogger import BNLogger 10 | # from delphi_ninja.bnhelpers import BNHelpers 11 | 12 | module_dir = path.dirname(path.dirname(path.abspath(__file__))) 13 | module_name = path.basename(module_dir) 14 | module_parent = path.dirname(module_dir) 15 | sys.path.insert(0, module_parent) 16 | delphi_ninja = importlib.import_module(module_name) 17 | DelphiAnalyzer = delphi_ninja.delphi_analyser.DelphiAnalyzer 18 | DelphiVMT = delphi_ninja.delphi_analyser.DelphiVMT 19 | BNLogger = delphi_ninja.bnlogger.BNLogger 20 | BNHelpers = delphi_ninja.bnhelpers.BNHelpers 21 | 22 | 23 | def analyze_callback(vmt: DelphiVMT, bv: BinaryView): 24 | BNLogger.log(f'Creating type for: {vmt}') 25 | BNHelpers.create_vmt_struct(bv, vmt) 26 | 27 | 28 | def main(target: str, delphi_version: int): 29 | # Just disable some features for large binaries 30 | opts = { 31 | 'analysis.mode': 'full', 32 | 'analysis.linearSweep.autorun': False, 33 | 'analysis.linearSweep.controlFlowGraph': False, 34 | } 35 | 36 | bv = BinaryViewType.get_view_of_file_with_options(target, options=opts) 37 | 38 | if bv is None: 39 | print(f'Invalid binary path: {target}') 40 | exit(-1) 41 | 42 | BNLogger.init_console() 43 | BNLogger.log('File loaded') 44 | BNLogger.log('-----------------------------') 45 | BNLogger.log('Searching for VMT...') 46 | 47 | analyzer = DelphiAnalyzer(bv, delphi_version) 48 | analyzer.update_analysis_and_wait(lambda vmt: analyze_callback(vmt, bv)) 49 | 50 | bv.update_analysis_and_wait() 51 | 52 | BNLogger.log(f'Saving database: `{target}.bndb`...') 53 | bv.create_database(f'{target}.bndb') 54 | 55 | 56 | if __name__ == '__main__': 57 | if len(sys.argv) != 3: 58 | print(f'Usage: {sys.argv[0]} ') 59 | exit(-1) 60 | 61 | main(sys.argv[1], int(sys.argv[2])) 62 | -------------------------------------------------------------------------------- /examples/list_vmts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from binaryninja import BinaryViewType 3 | 4 | import importlib 5 | import sys 6 | from os import path 7 | 8 | # from delphi_ninja.bnlogger import BNLogger 9 | # from delphi_ninja.delphi_analyser import DelphiAnalyzer 10 | 11 | module_dir = path.dirname(path.dirname(path.abspath(__file__))) 12 | module_name = path.basename(module_dir) 13 | module_parent = path.dirname(module_dir) 14 | sys.path.insert(0, module_parent) 15 | delphi_ninja = importlib.import_module(module_name) 16 | BNLogger = delphi_ninja.bnlogger.BNLogger 17 | DelphiAnalyzer = delphi_ninja.delphi_analyser.DelphiAnalyzer 18 | 19 | 20 | def main(target: str, delphi_version: int): 21 | # Just disable some features for large binaries 22 | opts = { 23 | 'analysis.mode': 'controlFlow', 24 | 'analysis.linearSweep.autorun': False, 25 | 'analysis.linearSweep.controlFlowGraph': False, 26 | } 27 | 28 | bv = BinaryViewType.get_view_of_file_with_options(target, options=opts) 29 | 30 | if bv is None: 31 | print(f'Invalid binary path: {target}') 32 | exit(-1) 33 | 34 | BNLogger.init_console() 35 | BNLogger.log('File loaded') 36 | BNLogger.log('-----------------------------') 37 | BNLogger.log('Searching for VMT...') 38 | 39 | analyzer = DelphiAnalyzer(bv, delphi_version) 40 | analyzer.update_analysis_and_wait(lambda vmt: BNLogger.log(vmt)) 41 | 42 | 43 | if __name__ == '__main__': 44 | if len(sys.argv) != 3: 45 | print(f'Usage: {sys.argv[0]} ') 46 | exit(-1) 47 | 48 | main(sys.argv[1], int(sys.argv[2])) 49 | -------------------------------------------------------------------------------- /examples/vmt_visualizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from binaryninja import BinaryViewType, LogLevel 3 | from graphviz import Digraph 4 | from typing import Dict 5 | 6 | import importlib 7 | import sys 8 | from os import path 9 | 10 | # from delphi_ninja.bnlogger import BNLogger 11 | # from delphi_ninja.delphi_analyser import DelphiAnalyzer, DelphiVMT 12 | 13 | module_dir = path.dirname(path.dirname(path.abspath(__file__))) 14 | module_name = path.basename(module_dir) 15 | module_parent = path.dirname(module_dir) 16 | sys.path.insert(0, module_parent) 17 | delphi_ninja = importlib.import_module(module_name) 18 | 19 | BNLogger = delphi_ninja.bnlogger.BNLogger 20 | DelphiAnalyzer = delphi_ninja.delphi_analyser.DelphiAnalyzer 21 | DelphiVMT = delphi_ninja.delphi_analyser.DelphiVMT 22 | 23 | 24 | def create_graph(vmt_map: Dict[int, DelphiVMT]): 25 | g = Digraph('VMT') 26 | 27 | for vmt in vmt_map.values(): 28 | if vmt.parent_vmt == 0 and vmt.class_name == 'TObject': 29 | continue 30 | 31 | if vmt.parent_vmt == 0: 32 | raise RuntimeError('The top level parent must be TObject') 33 | 34 | if vmt.parent_vmt not in vmt_map: 35 | BNLogger.log(f'Unknown parent at address {hex(vmt.parent_vmt)} for {vmt.class_name}', LogLevel.WarningLog) 36 | continue 37 | 38 | g.edge(vmt_map[vmt.parent_vmt].class_name, vmt.class_name) 39 | 40 | g.view('VMT', cleanup=True) 41 | 42 | 43 | def main(target: str, delphi_version: int): 44 | # Just disable some features for large binaries 45 | opts = { 46 | 'analysis.mode': 'controlFlow', 47 | 'analysis.linearSweep.autorun': False, 48 | 'analysis.linearSweep.controlFlowGraph': False, 49 | } 50 | 51 | bv = BinaryViewType.get_view_of_file_with_options(target, options=opts) 52 | 53 | if bv is None: 54 | print(f'Invalid binary path: {target}') 55 | exit(-1) 56 | 57 | BNLogger.init_console() 58 | BNLogger.log('File loaded') 59 | BNLogger.log('Searching for VMT...') 60 | 61 | analyzer = DelphiAnalyzer(bv, delphi_version) 62 | analyzer.update_analysis_and_wait() 63 | 64 | BNLogger.log('Creating Graph...') 65 | vmt_map = {vmt.start:vmt for vmt in analyzer.vmt_list} 66 | create_graph(vmt_map) 67 | 68 | 69 | if __name__ == '__main__': 70 | if len(sys.argv) != 3: 71 | print(f'Usage: {sys.argv[0]} ') 72 | exit(-1) 73 | 74 | main(sys.argv[1], int(sys.argv[2])) 75 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginmetadataversion": 2, 3 | "name": "Delphi VMT Analyzer", 4 | "author": "ImNotAVirus", 5 | "type": [ 6 | "helper" 7 | ], 8 | "api": [ 9 | "python3" 10 | ], 11 | "description": "Find Delphi VMTs and generate their structures", 12 | "license": { 13 | "name": "MIT", 14 | "text": "Copyright 2020 ImNotAVirus\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." 15 | }, 16 | "platforms": [ 17 | "Darwin", 18 | "Windows", 19 | "Linux" 20 | ], 21 | "installinstructions": { 22 | "Darwin": "No special instructions, package manager is recommended", 23 | "Windows": "No special instructions, package manager is recommended", 24 | "Linux": "No special instructions, package manager is recommended" 25 | }, 26 | "version": "0.1.2", 27 | "minimumbinaryninjaversion": 2440 28 | } 29 | --------------------------------------------------------------------------------