├── DelphiHelper ├── __init__.py ├── util │ ├── exception.py │ ├── ida.py │ └── delphi.py ├── core │ ├── ClassResolver.py │ ├── FormViewer.py │ ├── DelphiClass_DynamicTable.py │ ├── DelphiClass_ClassTable.py │ ├── FieldEnum.py │ ├── DelphiClass_VMTTable.py │ ├── FuncStruct.py │ ├── DelphiClass_InitTable.py │ ├── EPFinder.py │ ├── DFMFinder.py │ ├── DelphiClass_IntfTable.py │ ├── DelphiClass_TypeInfo.py │ ├── DelphiClass_FieldTable.py │ ├── DelphiClass_MethodTable.py │ ├── DelphiClass.py │ ├── DelphiClass_TypeInfo_tkClass.py │ ├── DFMParser.py │ ├── IDRKBLoader.py │ ├── DelphiForm.py │ ├── DelphiClass_TypeInfo_tkRecord.py │ └── IDRKBParser.py └── ui │ └── FormViewerUI.py ├── img ├── VMT.PNG ├── EPFinder.PNG ├── LoadFile.PNG ├── CreateForm.PNG ├── FieldTable.PNG ├── MethodTable.PNG ├── VMTStructure.PNG ├── IDAStruct_VMT.PNG ├── CallVMTFunc_After.PNG ├── DelphiFormViewer.PNG ├── FieldInFunc_After.PNG ├── CallVMTFunc_Before.PNG ├── FieldInFunc_Before.PNG ├── IDAEnum_FieldTable.PNG └── VMTStructureParsed.PNG ├── ida-plugin.json ├── LICENSE ├── setup_IDRKB.py ├── DelphiHelper.py └── README.md /DelphiHelper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/VMT.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/VMT.PNG -------------------------------------------------------------------------------- /img/EPFinder.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/EPFinder.PNG -------------------------------------------------------------------------------- /img/LoadFile.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/LoadFile.PNG -------------------------------------------------------------------------------- /img/CreateForm.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/CreateForm.PNG -------------------------------------------------------------------------------- /img/FieldTable.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/FieldTable.PNG -------------------------------------------------------------------------------- /img/MethodTable.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/MethodTable.PNG -------------------------------------------------------------------------------- /img/VMTStructure.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/VMTStructure.PNG -------------------------------------------------------------------------------- /img/IDAStruct_VMT.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/IDAStruct_VMT.PNG -------------------------------------------------------------------------------- /img/CallVMTFunc_After.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/CallVMTFunc_After.PNG -------------------------------------------------------------------------------- /img/DelphiFormViewer.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/DelphiFormViewer.PNG -------------------------------------------------------------------------------- /img/FieldInFunc_After.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/FieldInFunc_After.PNG -------------------------------------------------------------------------------- /img/CallVMTFunc_Before.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/CallVMTFunc_Before.PNG -------------------------------------------------------------------------------- /img/FieldInFunc_Before.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/FieldInFunc_Before.PNG -------------------------------------------------------------------------------- /img/IDAEnum_FieldTable.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/IDAEnum_FieldTable.PNG -------------------------------------------------------------------------------- /img/VMTStructureParsed.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eset/DelphiHelper/HEAD/img/VMTStructureParsed.PNG -------------------------------------------------------------------------------- /DelphiHelper/util/exception.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements DelphiHelper exception 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | class DelphiHelperError(Exception): 10 | 11 | def __init__(self, msg: str = str(), msgType: int = 0) -> None: 12 | Exception.__init__(self, msg) 13 | self.msg = msg 14 | 15 | if msgType == 0: 16 | self.msgType = "[ERROR]" 17 | else: 18 | self.msgType = "[WARNING]" 19 | 20 | def print(self) -> None: 21 | if self.msg: 22 | print(f"{self.msgType} {self.msg}") 23 | -------------------------------------------------------------------------------- /ida-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDAMetadataDescriptorVersion": 1, 3 | "plugin": { 4 | "name": "DelphiHelper", 5 | "entryPoint": "DelphiHelper.py", 6 | "version": "1.21", 7 | "idaVersions": ">=8.4", 8 | "description": "Analyzes Delphi binaries with VMT parsing, form viewing, and IDR Knowledge Base integration.", 9 | "license": "BSD 2-Clause", 10 | "pythonDependencies": ["py7zr"], 11 | "categories": [ 12 | "file-parsers-and-loaders", 13 | "decompilation", 14 | "ui-ux-and-visualization" 15 | ], 16 | "urls": { 17 | "repository": "https://github.com/eset/DelphiHelper" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Juraj Horňák", 22 | "email": "juraj.hornak@eset.com" 23 | }, 24 | { 25 | "name": "Tom Dupuy", 26 | "email": "thomas.dupuy@eset.com" 27 | } 28 | ], 29 | "keywords": [ 30 | "delphi", 31 | "borland", 32 | "embarcadero", 33 | "rtti", 34 | "vmt", 35 | "virtual-method-table", 36 | "dfm", 37 | "form-viewer", 38 | "idr", 39 | "knowledge-base", 40 | "pascal", 41 | "object-pascal", 42 | "windows-forms", 43 | "vcl", 44 | "rad-studio" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DelphiHelper/core/ClassResolver.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's VMT structures 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_idaapi 10 | import ida_kernwin 11 | import ida_name 12 | from DelphiHelper.core.DelphiClass import DelphiClass 13 | from DelphiHelper.util.delphi import GetApplicationClassAddr 14 | from DelphiHelper.util.exception import DelphiHelperError 15 | 16 | 17 | def ResolveApplicationClass( 18 | delphiVersion: int, 19 | classAddr: int = ida_idaapi.BADADDR) -> None: 20 | if classAddr == ida_idaapi.BADADDR: 21 | classAddr = GetApplicationClassAddr() 22 | if classAddr == ida_idaapi.BADADDR: 23 | return 24 | 25 | classApplicationName = ida_name.get_name(classAddr) 26 | if not classApplicationName.startswith('VMT_'): 27 | msg = "NODELAY\nHIDECANCEL\nProcessing \"TApplication\" VMT structure..." 28 | ida_kernwin.show_wait_box(msg) 29 | try: 30 | DelphiClass(classAddr, delphiVersion).MakeClass() 31 | except DelphiHelperError: 32 | pass 33 | finally: 34 | ida_kernwin.hide_wait_box() 35 | 36 | 37 | def ResolveClass(classAddr: int, delphiVersion: int) -> None: 38 | ResolveApplicationClass(delphiVersion) 39 | DelphiClass(classAddr, delphiVersion).MakeClass() 40 | -------------------------------------------------------------------------------- /DelphiHelper/core/FormViewer.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to show Delphi's forms extracted from DFMs 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_kernwin 10 | from DelphiHelper.core.DelphiForm import DelphiObject 11 | from DelphiHelper.ui.FormViewerUI import FormViewerUI 12 | 13 | 14 | class DelphiFormViewer(ida_kernwin.PluginForm): 15 | 16 | def __init__( 17 | self, 18 | formList: list[tuple[DelphiObject, list[tuple[str, int]], int]] 19 | ) -> None: 20 | ida_kernwin.PluginForm.__init__(self) 21 | self.parent = None 22 | self.__formList = formList 23 | 24 | def OnCreate(self, form) -> None: 25 | if len(self.__formList): 26 | FormViewerUI(self.FormToPyQtWidget(form), self.__formList) 27 | 28 | def OnClose(self, form) -> None: 29 | global _DelphiFormViewer 30 | del _DelphiFormViewer 31 | 32 | def Show(self): 33 | return ida_kernwin.PluginForm.Show( 34 | self, 35 | "Delphi Form Viewer", 36 | options=ida_kernwin.PluginForm.WOPN_PERSIST 37 | ) 38 | 39 | 40 | def FormViewer( 41 | formList: list[tuple[DelphiObject, list[tuple[str, int]], int]] 42 | ) -> None: 43 | global _DelphiFormViewer 44 | 45 | try: 46 | _DelphiFormViewer 47 | except Exception: 48 | _DelphiFormViewer = DelphiFormViewer(formList) 49 | 50 | _DelphiFormViewer.Show() 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2024, ESET 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_DynamicTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse Delphi's DynamicTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | from DelphiHelper.util.ida import * 11 | 12 | 13 | class DynamicTable(object): 14 | 15 | def __init__( 16 | self, 17 | classInfo: dict[str, str | dict[str, int]]) -> None: 18 | self.__tableAddr = classInfo["Address"]["DynamicTable"] 19 | self.__tableName = classInfo["Name"] 20 | self.__processorWordSize = GetProcessorWordSize() 21 | 22 | if self.__tableAddr != 0: 23 | self.__numOfEntries = Word(self.__tableAddr) 24 | 25 | def GetTableAddress(self) -> int: 26 | return self.__tableAddr 27 | 28 | def MakeTable(self) -> None: 29 | if self.__tableAddr != 0: 30 | self.__DeleteTable() 31 | self.__CreateTable() 32 | 33 | def __CreateTable(self) -> None: 34 | MakeWord(self.__tableAddr) 35 | MakeName(self.__tableAddr, self.__tableName + "_DynamicTable") 36 | 37 | for i in range(self.__numOfEntries): 38 | MakeWord(self.__tableAddr + 2 + 2 * i) 39 | 40 | for i in range(self.__numOfEntries): 41 | addr = (self.__tableAddr 42 | + 2 43 | + 2 * self.__numOfEntries 44 | + self.__processorWordSize * i) 45 | MakeCustomWord(addr, self.__processorWordSize) 46 | 47 | ida_bytes.set_cmt( 48 | self.__tableAddr, 49 | "Count", 50 | 0 51 | ) 52 | ida_bytes.set_cmt( 53 | self.__tableAddr + 2, 54 | "Method/Message handler number", 55 | 0 56 | ) 57 | ida_bytes.set_cmt( 58 | self.__tableAddr + 2 + 2 * self.__numOfEntries, 59 | "Method/Message handler pointer", 60 | 0 61 | ) 62 | 63 | def __DeleteTable(self) -> None: 64 | ida_bytes.del_items( 65 | self.__tableAddr, 66 | ida_bytes.DELIT_DELNAMES, 67 | self.__numOfEntries * (2 + self.__processorWordSize) + 2 68 | ) 69 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_ClassTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse Delphi's ClassTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import ida_name 11 | from DelphiHelper.util.exception import DelphiHelperError 12 | from DelphiHelper.util.ida import * 13 | 14 | 15 | class ClassTable(object): 16 | 17 | def __init__( 18 | self, 19 | addr: int, 20 | tableName: str, 21 | delphiVersion: int) -> None: 22 | self.__tableAddr = addr 23 | self.__tableName = tableName 24 | self.__tableEntries = list() 25 | self.__processorWordSize = GetProcessorWordSize() 26 | self.__delphiVersion = delphiVersion 27 | 28 | if self.__tableAddr != 0: 29 | self.__numOfEntries = Word(self.__tableAddr) 30 | 31 | def GetTableAddress(self) -> int: 32 | return self.__tableAddr 33 | 34 | def MakeTable(self) -> None: 35 | if self.__tableAddr != 0: 36 | self.__DeleteTable() 37 | self.__CreateTable() 38 | 39 | return self.__tableEntries 40 | 41 | def __CreateTable(self) -> None: 42 | MakeWord(self.__tableAddr) 43 | MakeName(self.__tableAddr, self.__tableName + "_ClassTable") 44 | ida_bytes.set_cmt(self.__tableAddr, "Number of entries", 0) 45 | 46 | addr = self.__tableAddr + 2 47 | 48 | for i in range(self.__numOfEntries): 49 | vmtStructAddr = GetCustomWord(addr, self.__processorWordSize) 50 | self.__tableEntries.append(vmtStructAddr) 51 | 52 | if ida_bytes.is_loaded(vmtStructAddr) and \ 53 | vmtStructAddr != 0 and \ 54 | ida_name.get_name(vmtStructAddr)[:4] != "VMT_": 55 | from DelphiHelper.core.DelphiClass import DelphiClass 56 | try: 57 | delphiClass = DelphiClass( 58 | vmtStructAddr, 59 | self.__delphiVersion 60 | ) 61 | delphiClass.MakeClass() 62 | except DelphiHelperError as e: 63 | e.print() 64 | 65 | MakeCustomWord(addr, self.__processorWordSize) 66 | addr += self.__processorWordSize 67 | 68 | def __DeleteTable(self) -> None: 69 | ida_bytes.del_items( 70 | self.__tableAddr, 71 | ida_bytes.DELIT_DELNAMES, 72 | self.__numOfEntries * self.__processorWordSize + 2 73 | ) 74 | -------------------------------------------------------------------------------- /DelphiHelper/core/FieldEnum.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to store data from Delphi's FieldTable in IDA's enums 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import idc 11 | import ida_idaapi 12 | from DelphiHelper.util.ida import FixName 13 | 14 | 15 | class FieldEnum(object): 16 | 17 | def __init__(self, enumName: str, enumComment: str) -> None: 18 | self.__fieldEnumName = enumName 19 | self.__fieldEnumComment = enumComment 20 | self.__fieldEnumId = ida_idaapi.BADADDR 21 | 22 | def AddMember( 23 | self, 24 | memberType: str, 25 | memberName: str, 26 | enumMemberValue: int) -> None: 27 | self.__CreateFieldEnum() 28 | self.__AddFieldEnumMember( 29 | memberType, 30 | memberName, 31 | enumMemberValue 32 | ) 33 | 34 | def __CreateFieldEnum(self) -> None: 35 | if self.__fieldEnumId == ida_idaapi.BADADDR: 36 | self.__DeleteEnum() 37 | 38 | self.__fieldEnumId = idc.add_enum( 39 | ida_idaapi.BADADDR, 40 | self.__fieldEnumName + "_Fields", 41 | ida_bytes.hex_flag() 42 | ) 43 | idc.set_enum_cmt(self.__fieldEnumId, self.__fieldEnumComment, 0) 44 | 45 | def __AddFieldEnumMember( 46 | self, 47 | memberType: str, 48 | memberName: str, 49 | enumMemberValue: int) -> None: 50 | enumMemberName = (self.__fieldEnumName 51 | + "_" 52 | + memberType 53 | + "_" 54 | + memberName) 55 | enumMemberName = FixName(enumMemberName) 56 | 57 | memberId = idc.get_enum_member_by_name(enumMemberName) 58 | 59 | if memberId == ida_idaapi.BADADDR: 60 | idc.add_enum_member( 61 | self.__fieldEnumId, 62 | enumMemberName, 63 | enumMemberValue, 64 | -1 65 | ) 66 | elif idc.get_enum_member_value(memberId) != enumMemberValue: 67 | self.__AddFieldEnumMember( 68 | memberType, 69 | memberName + "_", 70 | enumMemberValue 71 | ) 72 | 73 | def __DeleteEnum(self) -> None: 74 | fieldEnumId = idc.get_enum(self.__fieldEnumName + "_Fields") 75 | 76 | if fieldEnumId != ida_idaapi.BADADDR: 77 | idc.del_enum(fieldEnumId) 78 | 79 | self.__fieldEnumId = ida_idaapi.BADADDR 80 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_VMTTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's VMTTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | from DelphiHelper.core.FuncStruct import FuncStruct 11 | from DelphiHelper.util.delphi import DemangleFuncName 12 | from DelphiHelper.util.ida import * 13 | 14 | 15 | class VMTTable(object): 16 | 17 | def __init__( 18 | self, 19 | classInfo: dict[str, str | dict[str, int]], 20 | funcStruct: FuncStruct) -> None: 21 | self.__tableAddr = classInfo["Address"]["VMTTable"] 22 | self.__tableName = classInfo["Name"] 23 | self.__funcStruct = funcStruct 24 | self.__processorWordSize = GetProcessorWordSize() 25 | 26 | if classInfo["Address"]["InitTable"] != 0: 27 | self.__tableEndAddr = classInfo["Address"]["InitTable"] 28 | elif classInfo["Address"]["FieldTable"] != 0: 29 | self.__tableEndAddr = classInfo["Address"]["FieldTable"] 30 | elif classInfo["Address"]["MethodTable"] != 0: 31 | self.__tableEndAddr = classInfo["Address"]["MethodTable"] 32 | elif classInfo["Address"]["DynamicTable"] != 0: 33 | self.__tableEndAddr = classInfo["Address"]["DynamicTable"] 34 | elif classInfo["Address"]["ClassName"] != 0: 35 | self.__tableEndAddr = classInfo["Address"]["ClassName"] 36 | else: 37 | self.__tableEndAddr = self.__tableAddr 38 | 39 | def GetTableAddress(self) -> int: 40 | return self.__tableAddr 41 | 42 | def MakeTable(self) -> None: 43 | if self.__tableAddr != 0: 44 | self.__DeleteTable() 45 | self.__CreateTableAndExtractData() 46 | 47 | def __CreateTableAndExtractData(self) -> None: 48 | MakeName(self.__tableAddr, self.__tableName + "_VMT") 49 | 50 | offset = 0 51 | while self.__tableAddr + offset < self.__tableEndAddr: 52 | funcAddr = GetCustomWord( 53 | self.__tableAddr + offset, 54 | self.__processorWordSize 55 | ) 56 | 57 | MakeFunction(funcAddr) 58 | DemangleFuncName(funcAddr) 59 | MakeCustomWord(self.__tableAddr + offset, self.__processorWordSize) 60 | 61 | ida_bytes.set_cmt( 62 | self.__tableAddr + offset, 63 | "'+" + ("%x" % offset).upper() + "'", 64 | 0 65 | ) 66 | 67 | self.__funcStruct.AddMember(funcAddr, offset) 68 | 69 | offset += self.__processorWordSize 70 | 71 | def __DeleteTable(self) -> None: 72 | ida_bytes.del_items( 73 | self.__tableAddr, 74 | ida_bytes.DELIT_DELNAMES, 75 | self.__tableEndAddr - self.__tableAddr 76 | ) 77 | -------------------------------------------------------------------------------- /DelphiHelper/core/FuncStruct.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to store data extracted from Delphi's RTTI tables into 3 | # IDA's structures 4 | # 5 | # Copyright (c) 2020-2025 ESET 6 | # Author: Juraj Horňák 7 | # See LICENSE file for redistribution. 8 | 9 | 10 | import ida_bytes 11 | import ida_idaapi 12 | import ida_name 13 | import idc 14 | from DelphiHelper.util.ida import GetProcessorWordSize 15 | 16 | 17 | class FuncStruct(object): 18 | 19 | def __init__(self, funcStructName: str, funcStructComment: str) -> None: 20 | self.__funcStructName = funcStructName 21 | self.__funcStructComment = funcStructComment 22 | self.__funcStructId = ida_idaapi.BADADDR 23 | self.__processorWordSize = GetProcessorWordSize() 24 | 25 | def AddMember(self, funcAddr: int, funcOffset: int) -> None: 26 | self.__CreateFuncStruct() 27 | self.__AddFuncStructMember(funcAddr, funcOffset) 28 | 29 | def __CreateFuncStruct(self) -> None: 30 | if self.__funcStructId == ida_idaapi.BADADDR: 31 | self.__DeleteStruct() 32 | self.__funcStructId = idc.add_struc(-1, self.__funcStructName, 0) 33 | 34 | idc.set_struc_cmt( 35 | self.__funcStructId, 36 | self.__funcStructComment, 37 | 0 38 | ) 39 | 40 | def __AddFuncStructMember(self, funcAddr: int, funcOffset: int) -> None: 41 | funcName = ida_name.get_name(funcAddr) 42 | 43 | if self.__processorWordSize == 4: 44 | idc.add_struc_member( 45 | self.__funcStructId, 46 | funcName + "_" + hex(funcOffset), 47 | funcOffset, 48 | ida_bytes.FF_DWORD, 49 | -1, 50 | 4 51 | ) 52 | else: 53 | idc.add_struc_member( 54 | self.__funcStructId, 55 | funcName + "_" + hex(funcOffset), 56 | funcOffset, 57 | ida_bytes.FF_QWORD, 58 | -1, 59 | 8 60 | ) 61 | 62 | memberId = idc.get_member_id(self.__funcStructId, funcOffset) 63 | 64 | idc.set_member_cmt( 65 | self.__funcStructId, 66 | funcOffset, 67 | "0x" + ("%x" % funcAddr), 68 | 1 69 | ) 70 | 71 | structMemberPrototype = self.__GetStructMemberPrototype(funcName) 72 | if structMemberPrototype is not None: 73 | idc.SetType(memberId, structMemberPrototype) 74 | 75 | def __GetStructMemberPrototype(self, funcName: str) -> str | None: 76 | structMemberPrototype = idc.get_type(idc.get_name_ea_simple(funcName)) 77 | 78 | if structMemberPrototype is not None: 79 | funcNameStart = structMemberPrototype.find("(") 80 | 81 | if funcNameStart != -1 and \ 82 | structMemberPrototype[funcNameStart - 1] != " ": 83 | structMemberPrototype = (structMemberPrototype[0: funcNameStart] 84 | + "(*" 85 | + funcName 86 | + ")" 87 | + structMemberPrototype[funcNameStart:]) 88 | 89 | return structMemberPrototype 90 | 91 | def __DeleteStruct(self) -> None: 92 | structId = idc.get_struc_id(self.__funcStructName) 93 | 94 | if structId != ida_idaapi.BADADDR: 95 | idc.del_struc(structId) 96 | 97 | self.__funcStructId = ida_idaapi.BADADDR 98 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_InitTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's InitTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | from DelphiHelper.core.DelphiClass_TypeInfo import * 11 | from DelphiHelper.util.ida import * 12 | 13 | 14 | class InitTable(object): 15 | 16 | def __init__( 17 | self, 18 | delphiVersion: int, 19 | classInfo: dict[str, str | dict[str, int]], 20 | fieldEnum: FieldEnum) -> None: 21 | self.__tableAddr = classInfo["Address"]["InitTable"] 22 | self.__fieldEnum = fieldEnum 23 | self.__tableName = classInfo["Name"] 24 | self.__processorWordSize = GetProcessorWordSize() 25 | self.__delphiVersion = delphiVersion 26 | 27 | if self.__tableAddr != 0: 28 | self.__fieldCount = Word(self.__tableAddr + 6) 29 | self.__fieldAddr = self.__tableAddr + 8 30 | 31 | def GetTableAddress(self) -> int: 32 | return self.__tableAddr 33 | 34 | def MakeTable(self) -> None: 35 | if self.__tableAddr != 0: 36 | self.__DeleteTable() 37 | self.__CreateTable() 38 | self.__ExtractData() 39 | 40 | def __CreateTable(self) -> None: 41 | MakeWord(self.__tableAddr) 42 | MakeWord(self.__tableAddr + 2) 43 | MakeWord(self.__tableAddr + 4) 44 | MakeWord(self.__tableAddr + 6) 45 | MakeName(self.__tableAddr, self.__tableName + "_InitTable") 46 | 47 | addr = self.__fieldAddr 48 | for i in range(self.__fieldCount): 49 | MakeWord(addr) 50 | MakeCustomWord(addr + 2, self.__processorWordSize) 51 | 52 | if self.__processorWordSize == 4: 53 | MakeWord(addr + 6) 54 | addr += 8 55 | else: 56 | MakeWord(addr + 10) 57 | MakeDword(addr + 12) 58 | addr += 16 59 | 60 | def __DeleteTable(self) -> None: 61 | ida_bytes.del_items(self.__tableAddr, ida_bytes.DELIT_DELNAMES, 8) 62 | 63 | addr = self.__fieldAddr 64 | for i in range(self.__fieldCount): 65 | if self.__processorWordSize == 4: 66 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, 8) 67 | addr += 8 68 | else: 69 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, 16) 70 | addr += 16 71 | 72 | def __ExtractData(self) -> None: 73 | if self.__fieldCount != 0: 74 | addr = self.__fieldAddr 75 | 76 | for i in range(self.__fieldCount): 77 | typeInfoAddr = GetCustomWord( 78 | addr + 2, 79 | self.__processorWordSize 80 | ) 81 | 82 | if ida_bytes.is_loaded(typeInfoAddr) and \ 83 | typeInfoAddr != 0: 84 | typeInfo = TypeInfo( 85 | self.__delphiVersion, 86 | typeInfoAddr + self.__processorWordSize 87 | ) 88 | typeInfo.MakeTable(1) 89 | fieldType = typeInfo.GetTypeName() 90 | fieldValue = Word(addr + 2 + self.__processorWordSize) 91 | 92 | self.__fieldEnum.AddMember( 93 | fieldType, 94 | hex(fieldValue), 95 | fieldValue 96 | ) 97 | 98 | if self.__processorWordSize == 4: 99 | addr += 8 100 | else: 101 | addr += 16 102 | -------------------------------------------------------------------------------- /setup_IDRKB.py: -------------------------------------------------------------------------------- 1 | # 2 | # This script downloads IDR Knowledge Base files used by DelphiHelper plugin 3 | # 4 | # The script downloads KB files from IDR projects: 5 | # https://github.com/crypto2011/IDR, 6 | # https://github.com/crypto2011/IDR64, 7 | # which are licensed under the MIT License: 8 | # https://github.com/crypto2011/IDR/blob/master/LICENSE 9 | # 10 | # Copyright (c) 2020-2024 ESET 11 | # Author: Juraj Horňák 12 | # See LICENSE file for redistribution. 13 | 14 | 15 | import os 16 | import py7zr 17 | import shutil 18 | import sys 19 | import urllib.error 20 | import urllib.request 21 | 22 | 23 | IDRKBDIR = "IDR_KB" 24 | PLUGIN_PATH = os.path.expandvars("$HOME/.idapro/plugins/DelphiHelper/") 25 | if sys.platform == "win32": 26 | PLUGIN_PATH = os.path.expandvars("%APPDATA%\\Hex-Rays\\IDA Pro\\plugins\\DelphiHelper\\") 27 | IDR_PATH = os.path.join(PLUGIN_PATH, IDRKBDIR) 28 | 29 | DOWNLOADLIST_IDR = [ 30 | ("kb2005.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2005.7z"), 31 | ("kb2006.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2006.7z"), 32 | ("kb2007.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2007.7z"), 33 | ("kb2009.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2009.7z"), 34 | ("kb2010.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2010.7z"), 35 | ("kb2011.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2011.7z"), 36 | ("kb2012.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2012.7z"), 37 | ("kb2013.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2013.7z"), 38 | ("kb2014.7z", "https://raw.githubusercontent.com/crypto2011/IDR/master/kb2014.7z") 39 | ] 40 | 41 | DOWNLOADLIST_IDR64 = [ 42 | ("syskb2012.bin", "https://raw.githubusercontent.com/crypto2011/IDR64/master/syskb2012.bin"), 43 | ("syskb2013.bin", "https://raw.githubusercontent.com/crypto2011/IDR64/master/syskb2013.bin"), 44 | ("syskb2014.bin", "https://raw.githubusercontent.com/crypto2011/IDR64/master/syskb2014.bin") 45 | ] 46 | 47 | 48 | def downloadFile(url: str, filename: str) -> bool: 49 | print(f"[INFO] Downloading file from: {url}") 50 | 51 | try: 52 | urllib.request.urlretrieve(url, filename) 53 | except urllib.error.HTTPError as e: 54 | print(f"[ERROR] HTTPError: {e.msg} ({e.url})") 55 | return False 56 | 57 | print(f"[INFO] The file saved as \"{filename}\"") 58 | return True 59 | 60 | def unpack7z(archivePath: str, destinationFolder: str) -> bool: 61 | print(f"[INFO] Unpacking archive \"{archivePath}\"...") 62 | 63 | if not py7zr.is_7zfile(archivePath): 64 | print(f"[ERROR] {archivePath} is not valid 7z file.") 65 | return False 66 | 67 | with py7zr.SevenZipFile(archivePath, "r") as archive: 68 | archive.extractall(path=destinationFolder) 69 | 70 | return True 71 | 72 | def init() -> bool: 73 | try: 74 | if not os.path.exists(PLUGIN_PATH): 75 | print(f"[ERROR] DelphiHelper directory \"{PLUGIN_PATH}\" not found!") 76 | print("[INFO] Read the README.md for DelphiHelper installation location.") 77 | return False 78 | 79 | if os.path.exists(IDR_PATH): 80 | shutil.rmtree(IDR_PATH) 81 | 82 | os.mkdir(IDR_PATH) 83 | os.mkdir(os.path.join(IDR_PATH, "IDR")) 84 | os.mkdir(os.path.join(IDR_PATH, "IDR64")) 85 | except FileNotFoundError as e: 86 | raise e 87 | 88 | return True 89 | 90 | def downloadIDRKB() -> None: 91 | if init(): 92 | print("[INFO] Downloading IDR Knowledge Base files...") 93 | for filename, url in DOWNLOADLIST_IDR64: 94 | downloadFile(url, os.path.join(IDR_PATH, "IDR64", filename)) 95 | 96 | for filename, url in DOWNLOADLIST_IDR: 97 | downloadFile(url, filename) 98 | unpack7z(filename, os.path.join(IDR_PATH, "IDR")) 99 | os.remove(filename) 100 | 101 | downloadIDRKB() 102 | -------------------------------------------------------------------------------- /DelphiHelper/core/EPFinder.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements simple heuristic for searching for EP functions 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_idaapi 10 | import ida_kernwin 11 | import ida_name 12 | import ida_ua 13 | import idautils 14 | import idc 15 | from DelphiHelper.core.ClassResolver import * 16 | from DelphiHelper.core.DelphiClass import * 17 | from DelphiHelper.util.delphi import FindInitTable, GetApplicationClassAddr 18 | from DelphiHelper.util.exception import DelphiHelperError 19 | from DelphiHelper.util.ida import * 20 | 21 | 22 | class EPFinder(object): 23 | 24 | def __init__(self, delphiVersion: int) -> None: 25 | self.__delphiVersion = delphiVersion 26 | 27 | def FindEPFunction(self) -> None: 28 | createFormRef = dict() 29 | initExeRef = list() 30 | EPAddrToJmp = 0 31 | createFormFuncAddr = 0 32 | processorWordSize = GetProcessorWordSize() 33 | 34 | classAddr = GetApplicationClassAddr() 35 | 36 | if classAddr != ida_idaapi.BADADDR: 37 | ResolveApplicationClass(self.__delphiVersion, classAddr) 38 | # CreateForm 39 | createFormStrAddr = find_bytes("0A 43 72 65 61 74 65 46 6F 72 6D") 40 | 41 | if createFormStrAddr != ida_idaapi.BADADDR: 42 | createFormFuncAddr = GetCustomWord( 43 | createFormStrAddr - processorWordSize, 44 | processorWordSize 45 | ) 46 | 47 | if createFormFuncAddr == 0: 48 | for funcAddr in idautils.Functions(): 49 | funcName = ida_name.get_name(funcAddr) 50 | 51 | if "TApplication" in funcName and "CreateForm" in funcName: 52 | createFormFuncAddr = funcAddr 53 | break 54 | 55 | if createFormFuncAddr != 0: 56 | for ref in idautils.CodeRefsTo(createFormFuncAddr, 1): 57 | createFormRef[ref] = self.__getFormName(ref) 58 | 59 | initTable = FindInitTable() 60 | if initTable: 61 | for ref in idautils.DataRefsTo(initTable): 62 | initExeRef.append(ref) 63 | 64 | if initExeRef or createFormRef: 65 | i = 0 66 | print("[INFO] Found EP functions:") 67 | 68 | if initExeRef: 69 | EPAddrToJmp = initExeRef[0] 70 | 71 | for addr in initExeRef: 72 | print(f"[INFO] EP{i}: 0x{addr:x} InitExe/InitLib") 73 | i += 1 74 | 75 | if len(createFormRef): 76 | for addr, name in createFormRef.items(): 77 | if EPAddrToJmp == 0: 78 | EPAddrToJmp = addr 79 | 80 | if name == "": 81 | print(f"[INFO] EP{i}: 0x{addr:x}") 82 | else: 83 | print(f"[INFO] EP{i}: 0x{addr:x} CreateForm(\"{name}\")") 84 | i += 1 85 | 86 | if EPAddrToJmp != 0: 87 | ida_kernwin.jumpto(EPAddrToJmp) 88 | else: 89 | print("[INFO] EP function not found!") 90 | 91 | def __getFormName(self, createFormRef: int) -> str: 92 | addr = createFormRef 93 | regEdx = "edx" 94 | if Is64bit(): 95 | regEdx = "rdx" 96 | 97 | counter = 3 98 | while counter != 0: 99 | addr = idc.prev_head(addr) 100 | if ida_ua.print_insn_mnem(addr) == "mov" and \ 101 | idc.print_operand(addr, 0) == regEdx and \ 102 | idc.get_operand_type(addr, 1) == 0x2: 103 | addr = idc.get_operand_value(addr, 1) 104 | try: 105 | delphiClass = DelphiClass( 106 | addr, 107 | self.__delphiVersion 108 | ) 109 | if not ida_name.get_name(addr).startswith("VMT_"): 110 | delphiClass.MakeClass() 111 | return delphiClass.GetClassFullName() 112 | except DelphiHelperError: 113 | return "" 114 | counter -= 1 115 | return "" 116 | -------------------------------------------------------------------------------- /DelphiHelper/core/DFMFinder.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to search for Delphi's DFM in Delphi binary 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_idaapi 10 | import ida_kernwin 11 | import ida_nalt 12 | import ida_offset 13 | import idautils 14 | import struct 15 | from DelphiHelper.util.ida import Byte, Word, Dword, find_bytes 16 | 17 | 18 | class DFMFinder(): 19 | 20 | def __init__(self) -> None: 21 | self.__rsrcSecAddr = self.__GetResourceSectionAddress() 22 | self.__DFMlist = list() 23 | self.__ExtractDFM() 24 | 25 | def GetDFMList(self) -> list[tuple[int, int]]: 26 | return self.__DFMlist 27 | 28 | def __CheckDFMSignature(self, addr: int) -> bool: 29 | if chr(Byte(addr)) == "T" and \ 30 | chr(Byte(addr + 1)) == "P" and \ 31 | chr(Byte(addr + 2)) == "F" and \ 32 | chr(Byte(addr + 3)) == "0": 33 | return True 34 | else: 35 | return False 36 | 37 | def __GetResourceSectionAddress(self) -> int: 38 | pe = idautils.peutils_t() 39 | 40 | resourceDirectoryOffset = 0x88 41 | if not pe or len(pe.header()) < resourceDirectoryOffset + 4: 42 | return 0 43 | 44 | resourceDirectory = pe.header()[resourceDirectoryOffset:resourceDirectoryOffset+4] 45 | resourceDirectoryRVA = struct.unpack("i", resourceDirectory)[0] 46 | 47 | if resourceDirectoryRVA: 48 | return ida_nalt.get_imagebase() + resourceDirectoryRVA 49 | else: 50 | return 0 51 | 52 | def __GetRCDATAAddr(self) -> int: 53 | numOfDirEntries = self.__GetNumberOfDirEntries(self.__rsrcSecAddr) 54 | addr = self.__rsrcSecAddr + 16 55 | 56 | for i in range(numOfDirEntries): 57 | # RCDATA 58 | if Dword(addr) == 10 and Dword(addr + 4) & 0x80000000 != 0: 59 | return self.__rsrcSecAddr + (Dword(addr + 4) & 0x7FFFFFFF) 60 | addr += 8 61 | return 0 62 | 63 | def __GetNumberOfDirEntries(self, tableAddr: int) -> int: 64 | return Word(tableAddr + 12) + Word(tableAddr + 14) 65 | 66 | def __ExtractDFMFromResource(self) -> None: 67 | print("[INFO] Searching for DFM in loaded resource section...") 68 | 69 | if self.__rsrcSecAddr == 0: 70 | print("[INFO] The resource directory is empty.") 71 | return 72 | 73 | if ida_offset.can_be_off32(self.__rsrcSecAddr) != ida_idaapi.BADADDR: 74 | RCDATAaddr = self.__GetRCDATAAddr() 75 | 76 | if RCDATAaddr != 0: 77 | RCDATAaddrEntryCount = self.__GetNumberOfDirEntries(RCDATAaddr) 78 | addr = RCDATAaddr + 16 79 | 80 | for i in range(RCDATAaddrEntryCount): 81 | if Dword(addr) & 0x80000000 != 0 and \ 82 | Dword(addr + 4) & 0x80000000 != 0: 83 | dirTableAddr = (self.__rsrcSecAddr 84 | + (Dword(addr + 4) & 0x7FFFFFFF)) 85 | 86 | if self.__GetNumberOfDirEntries(dirTableAddr) == 1: 87 | DFMDataAddr = (ida_nalt.get_imagebase() 88 | + Dword(self.__rsrcSecAddr 89 | + Dword(dirTableAddr + 20))) 90 | 91 | DFMDataSizeAddr = (self.__rsrcSecAddr 92 | + Dword(dirTableAddr + 20) 93 | + 4) 94 | DFMDataSize = Dword(DFMDataSizeAddr) 95 | 96 | if self.__CheckDFMSignature(DFMDataAddr): 97 | self.__DFMlist.append((DFMDataAddr, DFMDataSize)) 98 | addr += 8 99 | else: 100 | print("[WARNING] The resource section seems to be corrupted!") 101 | else: 102 | print("[WARNING] The resource section not found! Make sure the resource section is loaded by IDA.") 103 | ida_kernwin.warning("The resource section not found!\nMake sure the resource section is loaded by IDA.") 104 | 105 | def __ExtractDFMFromBinary(self) -> None: 106 | print("[INFO] Searching for DFM in loaded binary...") 107 | 108 | self.__DFMlist = list() 109 | startAddr = 0 110 | counter = 0 111 | 112 | while True: 113 | # 0x0TPF0 114 | dfmAddr = find_bytes("00 54 50 46 30", startAddr) 115 | 116 | if dfmAddr == ida_idaapi.BADADDR: 117 | break 118 | 119 | if counter != 0 and Byte(dfmAddr + 5) != 0: # FP 120 | print(f"[INFO] Found DFM: 0x{dfmAddr:x}") 121 | self.__DFMlist.append((dfmAddr + 1, 10000000)) 122 | 123 | counter += 1 124 | startAddr = dfmAddr + 1 125 | 126 | def __ExtractDFM(self) -> None: 127 | self.__ExtractDFMFromResource() 128 | 129 | if len(self.__DFMlist) == 0: 130 | self.__ExtractDFMFromBinary() 131 | 132 | if len(self.__DFMlist) == 0: 133 | print("[INFO] DFM not found.") 134 | -------------------------------------------------------------------------------- /DelphiHelper/util/ida.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements simple IDA utilities 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_auto 10 | import ida_bytes 11 | import ida_funcs 12 | import ida_ida 13 | import ida_idaapi 14 | import ida_nalt 15 | import ida_name 16 | import ida_search 17 | import ida_ua 18 | import idaapi 19 | import idc 20 | 21 | 22 | def find_bytes( 23 | search: str, 24 | address: int = 0, 25 | forward_search: bool = True) -> int: 26 | # IDA 9.0 27 | if idaapi.IDA_SDK_VERSION >= 900: 28 | if forward_search: 29 | address = ida_bytes.find_bytes(search, address) 30 | else: 31 | address = ida_bytes.find_bytes( 32 | search, 33 | range_start=0, 34 | range_end=address, 35 | flags=ida_bytes.BIN_SEARCH_BACKWARD 36 | ) 37 | # IDA 8.4 38 | elif idaapi.IDA_SDK_VERSION == 840: 39 | if forward_search: 40 | address = ida_search.find_binary(address, -1, search, 0x10, ida_search.SEARCH_DOWN) 41 | else: 42 | address = ida_search.find_binary(0, address, search, 0x10, ida_search.SEARCH_UP) 43 | else: 44 | print("[WARNING] Unsupported IDA version. Supported versions: IDA 8.4+") 45 | return ida_idaapi.BADADDR 46 | 47 | return address 48 | 49 | 50 | def Is64bit() -> bool: 51 | return ida_ida.inf_is_64bit() 52 | 53 | 54 | def Is32bit() -> bool: 55 | return ida_ida.inf_is_32bit_exactly() 56 | 57 | 58 | def GetProcessorWordSize() -> int: 59 | if Is64bit() is True: 60 | return 8 61 | elif Is32bit() is True: 62 | return 4 63 | else: 64 | raise Exception("Unsupported word size!") 65 | 66 | 67 | def GetCustomWord(addr: int, wordSize: int = 4) -> int: 68 | if wordSize == 8: 69 | return Qword(addr) 70 | elif wordSize == 4: 71 | return Dword(addr) 72 | else: 73 | raise Exception("Unsupported word size!") 74 | 75 | 76 | def MakeCustomWord(addr: int, wordSize: int = 4) -> None: 77 | if wordSize == 8: 78 | MakeQword(addr) 79 | elif wordSize == 4: 80 | MakeDword(addr) 81 | else: 82 | raise Exception("Unsupported word size!") 83 | 84 | 85 | def GetProcessorWord(addr: int) -> int: 86 | wordSize = GetProcessorWordSize() 87 | if wordSize == 8: 88 | return Qword(addr) 89 | elif wordSize == 4: 90 | return Dword(addr) 91 | else: 92 | raise Exception("Unsupported word size!") 93 | 94 | 95 | def MakeProccesorWord(addr: int) -> None: 96 | wordSize = GetProcessorWordSize() 97 | if wordSize == 8: 98 | MakeQword(addr) 99 | elif wordSize == 4: 100 | MakeDword(addr) 101 | else: 102 | raise Exception("Unsupported word size!") 103 | 104 | 105 | def Byte(addr: int) -> int: 106 | return ida_bytes.get_wide_byte(addr) 107 | 108 | 109 | def Word(addr: int) -> int: 110 | return ida_bytes.get_wide_word(addr) 111 | 112 | 113 | def Dword(addr: int) -> int: 114 | return ida_bytes.get_wide_dword(addr) 115 | 116 | 117 | def Qword(addr: int) -> int: 118 | return ida_bytes.get_qword(addr) 119 | 120 | 121 | def MakeByte(addr: int) -> None: 122 | ida_bytes.create_data(addr, ida_bytes.FF_BYTE, 1, ida_idaapi.BADADDR) 123 | 124 | 125 | def MakeWord(addr: int) -> None: 126 | ida_bytes.create_data(addr, ida_bytes.FF_WORD, 2, ida_idaapi.BADADDR) 127 | 128 | 129 | def MakeDword(addr: int) -> None: 130 | ida_bytes.create_data(addr, ida_bytes.FF_DWORD, 4, ida_idaapi.BADADDR) 131 | 132 | 133 | def MakeQword(addr: int) -> None: 134 | ida_bytes.create_data(addr, ida_bytes.FF_QWORD, 8, ida_idaapi.BADADDR) 135 | 136 | 137 | def GetStr(addr: int, strType: int, strLen: int = -1) -> bytes | None: 138 | return ida_bytes.get_strlit_contents(addr, strLen, strType) 139 | 140 | 141 | def GetStr_PASCAL(addr: int) -> bytes | None: 142 | return GetStr(addr, ida_nalt.STRTYPE_PASCAL, Byte(addr) + 1).decode() 143 | 144 | 145 | def MakeStr(startAddr: int, endAddr: int = ida_idaapi.BADADDR) -> None: 146 | idc.create_strlit(startAddr, endAddr) 147 | 148 | 149 | def MakeStr_PASCAL(startAddr: int) -> None: 150 | oldStringType = idc.get_inf_attr(ida_ida.INF_STRTYPE) 151 | idc.set_inf_attr(ida_ida.INF_STRTYPE, ida_nalt.STRTYPE_PASCAL) 152 | MakeStr(startAddr, startAddr + Byte(startAddr) + 1) 153 | idc.set_inf_attr(ida_ida.INF_STRTYPE, oldStringType) 154 | 155 | 156 | def FindRef_Dword( 157 | fromAddr: int, 158 | dwordToFind: int, 159 | flag: int = ida_bytes.BIN_SEARCH_FORWARD) -> int: 160 | stringToFind = str() 161 | 162 | for i in range(4): 163 | stringToFind += "%02X " % ((dwordToFind >> 8*i) & 0xff) 164 | 165 | if flag == ida_bytes.BIN_SEARCH_FORWARD: 166 | return find_bytes(stringToFind, fromAddr) 167 | else: 168 | return find_bytes(stringToFind, fromAddr, False) 169 | 170 | 171 | def FindRef_Qword( 172 | fromAddr: int, 173 | qwordToFind: int, 174 | flag: int = ida_bytes.BIN_SEARCH_FORWARD) -> int: 175 | stringToFind = str() 176 | 177 | for i in range(8): 178 | stringToFind += "%02X " % ((qwordToFind >> 8*i) & 0xff) 179 | 180 | if flag == ida_bytes.BIN_SEARCH_FORWARD: 181 | return find_bytes(stringToFind, fromAddr) 182 | else: 183 | return find_bytes(stringToFind, fromAddr, False) 184 | 185 | 186 | def FixName(name: str) -> str: 187 | name = "".join(i for i in name if ord(i) < 128) 188 | for elem in [".", "<", ">", ":", ",", "%", "&", "{", "}"]: 189 | if elem in name: 190 | name = name.replace(elem, "_") 191 | return name 192 | 193 | 194 | def MakeName(addr: int, name: str) -> None: 195 | name = FixName(name) 196 | # ida_idaapi.SN_FORCE == 0x800 197 | ida_name.set_name( 198 | addr, 199 | name, 200 | ida_name.SN_NOCHECK | ida_name.SN_NOWARN | 0x800 201 | ) 202 | 203 | 204 | def MakeFunction(addr: int) -> None: 205 | ida_funcs.del_func(addr) 206 | ida_bytes.del_items(addr, ida_bytes.DELIT_SIMPLE, 1) 207 | ida_ua.create_insn(addr) 208 | ida_auto.auto_wait() 209 | ida_funcs.add_func(addr) 210 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_IntfTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse Delphi's IntfTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | import idc 9 | import ida_bytes 10 | from DelphiHelper.core.DelphiClass_TypeInfo import * 11 | from DelphiHelper.util.ida import * 12 | 13 | 14 | class IntfTable(object): 15 | 16 | def __init__( 17 | self, 18 | delphiVersion: int, 19 | classInfo: dict[str, str | dict[str, int]]) -> None: 20 | self.__tableAddr = classInfo["Address"]["IntfTable"] 21 | self.__classAddr = classInfo["Address"]["Class"] 22 | self.__tableName = classInfo["Name"] 23 | self.__processorWordSize = GetProcessorWordSize() 24 | self.__minIntfAddr = 0 25 | self.__delphiVersion = delphiVersion 26 | 27 | if self.__tableAddr != 0: 28 | self.__intfCount = Dword(self.__tableAddr) 29 | self.__typeInfoAddr = (self.__tableAddr 30 | + self.__processorWordSize 31 | + self.__intfCount * (16 + 3 * self.__processorWordSize)) 32 | 33 | addr = self.__tableAddr + self.__processorWordSize 34 | intfList = list() 35 | 36 | for i in range(self.__intfCount): 37 | if GetCustomWord(addr + 16, self.__processorWordSize) != 0: 38 | intf = GetCustomWord(addr + 16, self.__processorWordSize) 39 | intfList.append(intf) 40 | addr += 3 * self.__processorWordSize + 16 41 | 42 | self.__minIntfAddr = 0 43 | 44 | if len(intfList) != 0: 45 | self.__minIntfAddr = intfList[0] 46 | 47 | for intfAddr in intfList: 48 | if intfAddr < self.__minIntfAddr: 49 | self.__minIntfAddr = intfAddr 50 | 51 | def GetTableAddress(self) -> int: 52 | return self.__tableAddr 53 | 54 | def MakeTable(self) -> None: 55 | if self.__tableAddr != 0: 56 | self.__DeleteTable() 57 | self.__CreateTable() 58 | 59 | def __CreateTable(self) -> None: 60 | MakeCustomWord(self.__tableAddr, self.__processorWordSize) 61 | MakeName(self.__tableAddr, self.__tableName + "_IntfTable") 62 | ida_bytes.set_cmt(self.__tableAddr, "Count", 0) 63 | 64 | addr = self.__tableAddr + self.__processorWordSize 65 | for i in range(self.__intfCount): 66 | MakeDword(addr) 67 | idc.make_array(addr, 4) 68 | MakeName(addr, self.__tableName + "_Interface" + str(i)) 69 | 70 | MakeCustomWord( 71 | addr + 16, 72 | self.__processorWordSize 73 | ) 74 | 75 | MakeName( 76 | GetCustomWord(addr + 16, self.__processorWordSize), 77 | self.__tableName + "_Interface" + str(i) + "_VMT" 78 | ) 79 | 80 | MakeCustomWord( 81 | addr + self.__processorWordSize + 16, 82 | self.__processorWordSize 83 | ) 84 | 85 | MakeCustomWord( 86 | addr + 2 * self.__processorWordSize + 16, 87 | self.__processorWordSize 88 | ) 89 | 90 | addr += 3 * self.__processorWordSize + 16 91 | 92 | addr = (self.__typeInfoAddr 93 | + self.__intfCount * self.__processorWordSize) 94 | 95 | if addr <= self.__classAddr: 96 | addr = self.__typeInfoAddr 97 | 98 | for i in range(self.__intfCount): 99 | MakeCustomWord(addr, self.__processorWordSize) 100 | 101 | typeInfoAddr = GetCustomWord(addr, self.__processorWordSize) 102 | if ida_bytes.is_loaded(typeInfoAddr) and \ 103 | typeInfoAddr != 0 and \ 104 | "_TypeInfo" not in idc.get_name(typeInfoAddr): 105 | typeInfo = TypeInfo( 106 | self.__delphiVersion, 107 | typeInfoAddr + self.__processorWordSize 108 | ) 109 | typeInfo.MakeTable(1) 110 | 111 | addr += self.__processorWordSize 112 | 113 | if self.__minIntfAddr != 0: 114 | addr = self.__minIntfAddr 115 | 116 | while addr < self.__tableAddr: 117 | MakeFunction(GetCustomWord(addr, self.__processorWordSize)) 118 | MakeCustomWord(addr, self.__processorWordSize) 119 | addr += self.__processorWordSize 120 | 121 | if self.__intfCount != 0: 122 | ida_bytes.set_cmt( 123 | self.__tableAddr + self.__processorWordSize, 124 | "GUID", 125 | 0 126 | ) 127 | ida_bytes.set_cmt( 128 | self.__tableAddr + self.__processorWordSize + 16, 129 | "Interface VMT", 130 | 0 131 | ) 132 | ida_bytes.set_cmt( 133 | self.__tableAddr + 2 * self.__processorWordSize + 16, 134 | "Interface's hidden field offset", 135 | 0 136 | ) 137 | ida_bytes.set_cmt( 138 | self.__tableAddr + 3 * self.__processorWordSize + 16, 139 | "Property implementing interface", 140 | 0 141 | ) 142 | 143 | def __DeleteTable(self) -> None: 144 | nbytes = ((3 * self.__processorWordSize + 16) * self.__intfCount 145 | + self.__processorWordSize) 146 | 147 | ida_bytes.del_items( 148 | self.__tableAddr, 149 | ida_bytes.DELIT_DELNAMES, 150 | nbytes 151 | ) 152 | 153 | addr = (self.__typeInfoAddr 154 | + self.__intfCount * self.__processorWordSize) 155 | 156 | if addr <= self.__classAddr: 157 | ida_bytes.del_items( 158 | self.__typeInfoAddr, 159 | ida_bytes.DELIT_DELNAMES, 160 | self.__processorWordSize * self.__intfCount 161 | ) 162 | 163 | offset = self.__minIntfAddr - self.__tableAddr 164 | 165 | if self.__minIntfAddr != 0 and \ 166 | offset % self.__processorWordSize == 0: 167 | ida_bytes.del_items( 168 | self.__minIntfAddr, 169 | ida_bytes.DELIT_DELNAMES, 170 | self.__tableAddr - self.__minIntfAddr 171 | ) 172 | -------------------------------------------------------------------------------- /DelphiHelper/ui/FormViewerUI.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements GUI for FormViewer 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_kernwin 10 | from PyQt5 import QtGui, QtWidgets 11 | from DelphiHelper.core.DelphiForm import DelphiObject, DelphiProperty 12 | 13 | 14 | class FormViewerUI(): 15 | 16 | def __init__( 17 | self, 18 | parent, 19 | delphiFormList: list[tuple[DelphiObject, list[tuple[str, int]], int]] 20 | ) -> None: 21 | super().__init__() 22 | self.parent = parent 23 | self.initUI(delphiFormList) 24 | 25 | def initUI( 26 | self, 27 | delphiFormList: list[tuple[DelphiObject, list[tuple[str, int]], int]] 28 | ) -> None: 29 | self.tree = QtWidgets.QTreeWidget() 30 | self.tree.setHeaderLabels(("Delphi Forms (Hint: Bold nodes -> Delphi Events inside, Green nodes -> binary files inside)",)) 31 | self.tree.setColumnWidth(0, 30) 32 | self.tree.itemDoubleClicked.connect(OnDoubleClickItem) 33 | self.tree.itemClicked.connect(OnClickItem) 34 | 35 | layout = QtWidgets.QVBoxLayout() 36 | layout.addWidget(self.tree) 37 | self.parent.setLayout(layout) 38 | 39 | self.PopulateFormTree(delphiFormList) 40 | 41 | def BuildObject( 42 | self, 43 | parentNode, 44 | delphiObject: DelphiObject, 45 | methodList: list[tuple[str, int]], 46 | VMTAddr: int = 0, 47 | _type: int = 1002) -> None: 48 | propList = delphiObject.GetPropertyList() 49 | childObjList = delphiObject.GetChildObjectList() 50 | 51 | objNode = ObjectNodeTreeItem(parentNode, delphiObject, VMTAddr, _type) 52 | 53 | if propList != list(): 54 | propertyNode = PropertyNodeTreeItem(objNode) 55 | 56 | for prop in propList: 57 | PropertyTreeItem(propertyNode, prop, methodList) 58 | 59 | if childObjList != list(): 60 | for childObj in childObjList: 61 | self.BuildObject(objNode, childObj, methodList) 62 | 63 | def BuildForm( 64 | self, 65 | parentNode, 66 | delphiForm: tuple[DelphiObject, list[tuple[str, int]], int] 67 | ) -> None: 68 | self.BuildObject( 69 | parentNode, 70 | delphiForm[0], 71 | delphiForm[1], 72 | delphiForm[2], 73 | 1001 74 | ) 75 | 76 | def PopulateFormTree( 77 | self, 78 | delphiFormList: list[tuple[DelphiObject, list[tuple[str, int]], int]] 79 | ) -> None: 80 | self.tree.clear() 81 | 82 | for delphiForm in delphiFormList: 83 | self.BuildForm(self.tree, delphiForm) 84 | 85 | 86 | class ObjectNodeTreeItem(QtWidgets.QTreeWidgetItem): 87 | 88 | def __init__( 89 | self, 90 | parent, 91 | delphiObject: DelphiObject, 92 | VMTAddr: int, 93 | _type: int) -> None: 94 | super(ObjectNodeTreeItem, self).__init__(parent, _type) 95 | 96 | self.setData(0, 0x100, delphiObject) 97 | self.setText( 98 | 0, 99 | "%s (%s)" % ( 100 | delphiObject.GetObjectName(), 101 | delphiObject.GetClassName() 102 | ) 103 | ) 104 | self.setData(0, 0x101, VMTAddr) 105 | 106 | if VMTAddr != 0: 107 | self.setData(0, 3, "Click to go to the Form's VMT structure") 108 | 109 | 110 | class PropertyNodeTreeItem(QtWidgets.QTreeWidgetItem): 111 | 112 | def __init__(self, parent) -> None: 113 | super(PropertyNodeTreeItem, self).__init__(parent) 114 | self.setText(0, "Properties") 115 | 116 | 117 | class PropertyTreeItem(QtWidgets.QTreeWidgetItem): 118 | 119 | def __init__( 120 | self, 121 | parent, 122 | prop: DelphiProperty, 123 | methodList: list[tuple[str, int]]) -> None: 124 | super(PropertyTreeItem, self).__init__(parent, 1000) 125 | 126 | self.setText( 127 | 0, 128 | "%s (%s) = %s" % ( 129 | prop.GetName(), 130 | prop.GetTypeAsString(), 131 | prop.GetValueAsString() 132 | ) 133 | ) 134 | 135 | self.setData(0, 0x100, prop) 136 | 137 | if prop.GetTypeAsString() == "Binary": 138 | self.__setBinaryPropertyItem() 139 | 140 | if prop.GetTypeAsString() == "Ident" and \ 141 | prop.GetName().startswith("On"): 142 | self.__setEventPropertyItem(prop, methodList) 143 | 144 | def __setBinaryPropertyItem(self) -> None: 145 | self.setData( 146 | 0, 147 | 3, 148 | "The file has been saved into current working directory" 149 | ) 150 | self.setForeground(0, QtGui.QBrush(QtGui.QColor("green"))) 151 | 152 | node = self.parent() 153 | while node is not None: 154 | node.setForeground(0, QtGui.QBrush(QtGui.QColor("green"))) 155 | 156 | if node != self.parent(): 157 | cmtToAdd = "The component or its child component includes some binary file (follow green nodes to see binary file in \"Properties\")" 158 | cmt = node.data(0, 3) 159 | 160 | if cmt is None: 161 | node.setData(0, 3, cmtToAdd) 162 | elif cmtToAdd not in cmt: 163 | node.setData(0, 3, cmt + "\n" + cmtToAdd) 164 | 165 | node = node.parent() 166 | 167 | def __setEventPropertyItem( 168 | self, 169 | prop: DelphiProperty, 170 | methodList: list[tuple[str, int]]) -> None: 171 | myFont = QtGui.QFont() 172 | myFont.setBold(True) 173 | 174 | self.setData(0, 0x101, 0) 175 | 176 | for method in methodList: 177 | if method[0] == prop.GetValue().decode(): 178 | self.setData(0, 3, "Double-click to go to the Event handler") 179 | self.setData(0, 0x101, method[1]) 180 | self.setForeground(0, QtGui.QBrush(QtGui.QColor("blue"))) 181 | self.setFont(0, myFont) 182 | 183 | node = self.parent() 184 | while node is not None: 185 | node.setFont(0, myFont) 186 | 187 | if node != self.parent(): 188 | cmtToAdd = "The component or its child component has some Event defined (follow bold nodes to see blue colored Event in \"Properties\")" 189 | cmt = node.data(0, 3) 190 | 191 | if cmt is None: 192 | node.setData(0, 3, cmtToAdd) 193 | elif cmtToAdd not in cmt: 194 | node.setData(0, 3, cmt + "\n" + cmtToAdd) 195 | 196 | node = node.parent() 197 | 198 | 199 | def OnDoubleClickItem(item) -> None: 200 | if item.type() == 1000: 201 | propType = item.data(0, 0x100).GetTypeAsString() 202 | 203 | if propType == "Ident" and \ 204 | item.data(0, 0x100).GetName().startswith("On"): 205 | ida_kernwin.jumpto(item.data(0, 0x101)) 206 | 207 | 208 | def OnClickItem(item) -> None: 209 | if item.type() == 1001 and item.data(0, 0x101) != 0: 210 | ida_kernwin.jumpto(item.data(0, 0x101)) 211 | -------------------------------------------------------------------------------- /DelphiHelper/util/delphi.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements utilities for working with Delphi binaries 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import ida_funcs 11 | import ida_ida 12 | import ida_idaapi 13 | import ida_kernwin 14 | import ida_name 15 | from DelphiHelper.util.ida import ( 16 | Is32bit, Is64bit, GetStr_PASCAL, GetProcessorWordSize, GetProcessorWord, 17 | Byte, MakeStr_PASCAL, find_bytes, FindRef_Dword, FindRef_Qword, MakeName, 18 | FixName 19 | ) 20 | 21 | 22 | def GetApplicationClassAddr() -> int: 23 | # TApplication# 24 | strTApplicationAddr = find_bytes("0C 54 41 70 70 6C 69 63 61 74 69 6F 6E") 25 | 26 | if strTApplicationAddr != ida_idaapi.BADADDR: 27 | if Is32bit(): 28 | addr = FindRef_Dword( 29 | strTApplicationAddr, 30 | strTApplicationAddr, 31 | ida_bytes.BIN_SEARCH_BACKWARD 32 | ) 33 | 34 | if addr != ida_idaapi.BADADDR: 35 | return addr - 0x20 36 | 37 | if Is64bit(): 38 | addr = FindRef_Qword( 39 | strTApplicationAddr, 40 | strTApplicationAddr, 41 | ida_bytes.BIN_SEARCH_BACKWARD 42 | ) 43 | 44 | if addr != ida_idaapi.BADADDR: 45 | return addr - 0x40 46 | 47 | return ida_idaapi.BADADDR 48 | 49 | 50 | def LoadDelphiFLIRTSignatures() -> None: 51 | """Load Delphi 32bit FLIRT signatures""" 52 | sigList = ["bds8vcl", "bds8rw32", "bds8ext", "mssdk32"] 53 | loadedSigList = list() 54 | 55 | result = find_bytes( 56 | "20 62 65 20 72 75 6E 20 75 6E 64 65 72 20 57 69 6E 33 32 0D 0A" 57 | ) 58 | result_2 = find_bytes("07 42 6F 6F 6C 65 61 6E") 59 | result_3 = find_bytes("05 46 61 6C 73 65") 60 | 61 | if ida_idaapi.BADADDR != result or \ 62 | (ida_idaapi.BADADDR != result_2 and ida_idaapi.BADADDR != result_3): 63 | for i in range(ida_funcs.get_idasgn_qty()): 64 | loadedSigList.append(ida_funcs.get_idasgn_desc(i)[0]) 65 | 66 | for sig in sigList: 67 | if sig not in loadedSigList: 68 | ida_funcs.plan_to_apply_idasgn(sig) 69 | 70 | 71 | def GetUnits() -> list[str]: 72 | ida_kernwin.show_wait_box("Searching for imported Units...") 73 | units = list() 74 | initTableAddr = 0 75 | 76 | if Is64bit(): 77 | initTableAddr = _findInitTable(40) 78 | elif IsExtendedInitTab(): 79 | initTableAddr = _findInitTable(20) 80 | else: 81 | units = ["SysInit", "System"] 82 | 83 | if units == list(): 84 | numOfUnits = GetProcessorWord( 85 | initTableAddr + 4 * GetProcessorWordSize() 86 | ) 87 | addr = GetProcessorWord(initTableAddr + 5 * GetProcessorWordSize()) 88 | 89 | for i in range(numOfUnits): 90 | unitName = GetStr_PASCAL(addr) 91 | units.append(unitName) 92 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, Byte(addr) + 1) 93 | MakeStr_PASCAL(addr) 94 | addr += Byte(addr) + 1 95 | 96 | ida_kernwin.hide_wait_box() 97 | 98 | return units 99 | 100 | 101 | def FindInitTable() -> int: 102 | initTable = 0 103 | 104 | if Is64bit(): 105 | initTable = _findInitTable(40) 106 | else: 107 | initTable = _findInitTable(4) 108 | 109 | if initTable == 0: 110 | initTable = _findInitTable(20) 111 | 112 | return initTable 113 | 114 | 115 | def _findInitTable(offset: int) -> int: 116 | addr = ida_ida.inf_get_min_ea() 117 | maxAddr = ida_ida.inf_get_max_ea() 118 | initTable = 0 119 | 120 | procWordSize = GetProcessorWordSize() 121 | 122 | while addr < maxAddr - offset: 123 | initTable = GetProcessorWord(addr) 124 | 125 | if initTable == addr + offset: 126 | initTable -= offset + procWordSize 127 | num = GetProcessorWord(initTable) 128 | 129 | if num > 0 and num < 10000: 130 | pos = addr + offset 131 | 132 | for i in range(num): 133 | funcAddr = GetProcessorWord(pos) 134 | 135 | if funcAddr and not ida_bytes.is_loaded(funcAddr): 136 | initTable = 0 137 | break 138 | 139 | funcAddr = GetProcessorWord(pos + procWordSize) 140 | 141 | if funcAddr and not ida_bytes.is_loaded(funcAddr): 142 | initTable = 0 143 | break 144 | 145 | pos += 2 * procWordSize 146 | 147 | if initTable: 148 | break 149 | 150 | initTable = 0 151 | addr += procWordSize 152 | 153 | return initTable 154 | 155 | 156 | def IsExtendedInitTab() -> bool: 157 | if _findInitTable(4): 158 | return False 159 | if _findInitTable(20): 160 | return True 161 | else: 162 | return False 163 | 164 | 165 | def DemangleFuncName(funcAddr: int) -> str: 166 | funcName = ida_name.get_name(funcAddr) 167 | 168 | if "@" in funcName: 169 | funcNameSplitted = funcName.split("$") 170 | names = funcNameSplitted[0] 171 | 172 | parameters = "" 173 | if "$" in funcName: 174 | parameters = funcNameSplitted[1] 175 | 176 | namesSplitted = names.split("@") 177 | 178 | if namesSplitted[-1] == "": 179 | if namesSplitted[-2] == "": 180 | print(f"[WARNING] FixFuncName: Unmangling error - {funcName}") 181 | elif parameters == "bctr": 182 | funcName = namesSplitted[-2] + "_Constructor" 183 | elif parameters == "bdtr": 184 | funcName = namesSplitted[-2] + "_Destructor" 185 | else: 186 | print(f"[WARNING] FixFuncName: Unmangling error - {funcName}") 187 | elif namesSplitted[-1] == "": 188 | funcName = namesSplitted[-3] + "_" + namesSplitted[-1] 189 | else: 190 | funcName = namesSplitted[-2] + "_" + namesSplitted[-1] 191 | 192 | MakeName(funcAddr, FixName(funcName)) 193 | 194 | return ida_name.get_name(funcAddr) 195 | 196 | 197 | def ParseMangledFunctionName(mang_func_name: str) -> str: 198 | func_name_splitted = mang_func_name.split("$") 199 | tmp = func_name_splitted[0] 200 | tmp_splitted = tmp.split("@") 201 | used_keywords = [] 202 | new_function_name = "" 203 | last_iteration_inserted = False 204 | 205 | for name in tmp_splitted: 206 | if name == "": 207 | continue 208 | 209 | if last_iteration_inserted: 210 | new_function_name += "_" 211 | last_iteration_inserted = False 212 | 213 | if name not in used_keywords: 214 | new_function_name += name 215 | last_iteration_inserted = True 216 | used_keywords.append(name) 217 | 218 | return new_function_name 219 | 220 | 221 | def GetParamRegister(index: int) -> str: 222 | procWordSize = GetProcessorWordSize() 223 | regStr = str() 224 | 225 | if procWordSize == 4: 226 | if index == 0: 227 | regStr = "@" 228 | elif index == 1: 229 | regStr = "@" 230 | elif index == 2: 231 | regStr = "@" 232 | else: 233 | if index == 0: 234 | regStr = "@" 235 | elif index == 1: 236 | regStr = "@" 237 | elif index == 2: 238 | regStr = "@" 239 | elif index == 3: 240 | regStr = "@" 241 | 242 | return regStr 243 | -------------------------------------------------------------------------------- /DelphiHelper.py: -------------------------------------------------------------------------------- 1 | # 2 | # IDA plugin definition 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_auto 10 | import ida_idaapi 11 | import ida_kernwin 12 | import idautils 13 | from DelphiHelper.core.ClassResolver import ResolveClass, ResolveApplicationClass 14 | from DelphiHelper.core.DelphiClass_TypeInfo import ParseTypeInfo 15 | from DelphiHelper.core.DFMParser import ParseDFMs 16 | from DelphiHelper.core.EPFinder import * 17 | from DelphiHelper.core.FormViewer import FormViewer 18 | from DelphiHelper.core.IDRKBLoader import KBLoader 19 | from DelphiHelper.core.IDRKBParser import GetDelphiVersion 20 | from DelphiHelper.util.delphi import LoadDelphiFLIRTSignatures 21 | from DelphiHelper.util.exception import DelphiHelperError 22 | 23 | 24 | PLUGIN_NAME = "DelphiHelper" 25 | PLUGIN_VERSION = "1.21" 26 | PLUGIN_AUTHOR = "Juraj Hornak (juraj.hornak@eset.com)" 27 | 28 | 29 | class DelphiHelperPlugin(ida_idaapi.plugin_t): 30 | flags = ida_idaapi.PLUGIN_MOD | ida_idaapi.PLUGIN_MULTI 31 | comment = PLUGIN_NAME + " - IDA plugin simplifying the analysis of Delphi x86/x64 binaries" 32 | help = "IDA plugin simplifying the analysis of Delphi binaries" 33 | wanted_name = PLUGIN_NAME 34 | wanted_hotkey = "Alt+Shift+H" 35 | 36 | def init(self): 37 | addon = ida_kernwin.addon_info_t() 38 | addon.id = "delphi_helper" 39 | addon.name = PLUGIN_NAME 40 | addon.producer = PLUGIN_AUTHOR 41 | addon.url = "juraj.hornak@eset.com" 42 | addon.version = PLUGIN_VERSION 43 | ida_kernwin.register_addon(addon) 44 | 45 | LoadDelphiFLIRTSignatures() 46 | 47 | return DelphiHelperPluginMain() 48 | 49 | 50 | class DelphiHelperPluginMain(ida_idaapi.plugmod_t): 51 | 52 | def __init__(self) -> None: 53 | ida_idaapi.plugmod_t.__init__(self) 54 | 55 | self.__delphiFormList = list() 56 | self.__packageinfo = None 57 | self.__parseFlag = True 58 | self.__delphiVersion = 0 59 | 60 | self.hotkeys = [] 61 | self.hotkeys.append(ida_kernwin.add_hotkey("Alt+Shift+R", self.resolveClass)) 62 | self.hotkeys.append(ida_kernwin.add_hotkey("Alt+Shift+E", self.findEntryPointFunc)) 63 | self.hotkeys.append(ida_kernwin.add_hotkey("Alt+Shift+F", self.formViewer)) 64 | self.hotkeys.append(ida_kernwin.add_hotkey("Alt+Shift+S", self.loadIDRKBSignatures_main)) 65 | self.hotkeys.append(ida_kernwin.add_hotkey("Alt+Shift+A", self.loadIDRKBSignatures_custom)) 66 | 67 | def run(self, arg): 68 | self.printHelp() 69 | 70 | def printHelp(self) -> None: 71 | print("-"*100) 72 | print(f"{PLUGIN_NAME} ({PLUGIN_VERSION}) by {PLUGIN_AUTHOR}") 73 | print("Copyright (c) 2020-2024 ESET\n") 74 | print("IDA plugin simplifying the analysis of Delphi x86/x64 binaries") 75 | print("\nHotkeys:") 76 | 77 | print(" \"Alt + Shift + R\" - run VMT Parser in order to parse selected VMT structure") 78 | print(" Usage: Press it in disassembly window when the cursor is on the starting address of a VMT structure") 79 | print(" e.g. mov edx, VMT_offset --> starting address of the VMT structure") 80 | print(" call CreateForm\n") 81 | 82 | print(" \"Alt + Shift + F\" - run DFM Finder (show Delphi Form Viewer)") 83 | print(" Usage: Press it anywhere in the disassembly window") 84 | print(" Note: The resource section of Delphi file must be loaded by IDA\n") 85 | 86 | print(" \"Alt + Shift + E\" - run Entry Point Function Finder (searching for \"CreateForm\", \"InitExe\" and \"InitLib\" references)") 87 | print(" Usage: Press it anywhere in the disassembly window\n") 88 | 89 | print(" \"Alt + Shift + S\" - run IDR Knowledge Base Loader for \"SysInit\" and \"System\" unit") 90 | print(" Usage: Press it anywhere in the disassembly window") 91 | print(" Note: read the README.md for KB file location.\n") 92 | 93 | print(" \"Alt + Shift + A\" - run IDR Knowledge Base Loader for selected units") 94 | print(" Usage: Press it anywhere in the disassembly window") 95 | print(" Note: read the README.md for KB file location.") 96 | print("-"*100) 97 | 98 | def term(self) -> None: 99 | for hotkey in self.hotkeys: 100 | ida_kernwin.del_hotkey(hotkey) 101 | 102 | def findEntryPointFunc(self) -> None: 103 | msg = "NODELAY\nHIDECANCEL\nSearching for EP function..." 104 | ida_kernwin.show_wait_box(msg) 105 | try: 106 | self.getDelphiVersion() 107 | EPFinder(self.__delphiVersion).FindEPFunction() 108 | except DelphiHelperError as e: 109 | e.print() 110 | finally: 111 | ida_kernwin.hide_wait_box() 112 | 113 | def resolveClass(self) -> None: 114 | msg = "NODELAY\nHIDECANCEL\nProcessing selected VMT structure..." 115 | ida_kernwin.show_wait_box(msg) 116 | try: 117 | self.getDelphiVersion() 118 | LoadDelphiFLIRTSignatures() 119 | ida_auto.auto_wait() 120 | ResolveClass(ida_kernwin.get_screen_ea(), self.__delphiVersion) 121 | except DelphiHelperError as e: 122 | e.print() 123 | finally: 124 | ida_kernwin.hide_wait_box() 125 | 126 | def formViewer(self) -> None: 127 | msg = "NODELAY\nHIDECANCEL\nProcessing Delphi file's DFMs..." 128 | ida_kernwin.show_wait_box(msg) 129 | try: 130 | self.getDelphiVersion() 131 | LoadDelphiFLIRTSignatures() 132 | ida_auto.auto_wait() 133 | ResolveApplicationClass(self.__delphiVersion) 134 | 135 | if self.__parseFlag: 136 | self.__delphiFormList = ParseDFMs(self.__delphiVersion) 137 | self.__parseFlag = False 138 | 139 | self.processTypeInfoStructures() 140 | 141 | if self.__delphiFormList: 142 | FormViewer(self.__delphiFormList) 143 | else: 144 | print("[INFO] The Delphi binary seems to not contain any Delphi Form") 145 | 146 | except DelphiHelperError as e: 147 | e.print() 148 | finally: 149 | ida_kernwin.hide_wait_box() 150 | 151 | def loadIDRKBSignatures_custom(self) -> None: 152 | try: 153 | KBLoader(True) 154 | except DelphiHelperError as e: 155 | e.print() 156 | 157 | def loadIDRKBSignatures_main(self) -> None: 158 | try: 159 | KBLoader(False) 160 | except DelphiHelperError as e: 161 | e.print() 162 | 163 | def getDelphiVersion(self) -> None: 164 | if self.__delphiVersion == 0: 165 | self.__delphiVersion = GetDelphiVersion() 166 | 167 | def processTypeInfoStructures(self) -> None: 168 | msg = "NODELAY\nHIDECANCEL\nProcessing TypeInfo structures..." 169 | ida_kernwin.show_wait_box(msg) 170 | try: 171 | self.getDelphiVersion() 172 | 173 | for seg in idautils.Segments(): 174 | addr = seg + 5 175 | endAddr = idc.get_segm_end(seg) 176 | 177 | while addr != ida_idaapi.BADADDR and addr < endAddr: 178 | addr = ParseTypeInfo(addr, self.__delphiVersion) 179 | except DelphiHelperError as e: 180 | e.print() 181 | finally: 182 | ida_kernwin.hide_wait_box() 183 | 184 | 185 | def PLUGIN_ENTRY(): 186 | """Required plugin entry point for IDAPython Plugins. 187 | """ 188 | 189 | return DelphiHelperPlugin() 190 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_TypeInfo.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's TypeInfo 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import ida_idaapi 11 | import ida_name 12 | from DelphiHelper.core.DelphiClass_TypeInfo_tkClass import TypeInfo_tkClass 13 | from DelphiHelper.core.DelphiClass_TypeInfo_tkRecord import TypeInfo_tkRecord 14 | from DelphiHelper.core.FieldEnum import FieldEnum 15 | from DelphiHelper.util.exception import DelphiHelperError 16 | from DelphiHelper.util.ida import * 17 | 18 | 19 | typeKindList = ["tkUnknown", "tkInteger", "tkChar", "tkEnumeration", 20 | "tkFloat", "tkString", "tkSet", "tkClass", "tkMethod", 21 | "tkWChar", "tkLString", "tkLWString", "tkVariant", 22 | "tkArray", "tkRecord", "tkInterface", "tkInt64", 23 | "tkDynArray", "tkUString", "tkClassRef", "tkPointer", 24 | "tkProcedure", "tkMRecord"] 25 | 26 | def ParseTypeInfo(addr: int, delphiVersion: int) -> int: 27 | global typeKindList 28 | 29 | if Byte(addr) and \ 30 | Byte(addr) < len(typeKindList) and \ 31 | Dword(addr - GetProcessorWordSize()) == addr and \ 32 | Byte(addr - GetProcessorWordSize() - 1) == 0: 33 | typeInfo = TypeInfo(delphiVersion, addr) 34 | typeInfo.MakeTable() 35 | 36 | if typeKindList[Byte(addr)] == "tkClass": 37 | typeInfo.ResolveTypeInfo(addr) 38 | 39 | addr += Byte(addr + 1) 40 | 41 | return addr + 1 42 | 43 | class TypeInfo(object): 44 | 45 | def __init__( 46 | self, 47 | delphiVersion: int, 48 | addr: int = ida_idaapi.BADADDR, 49 | fieldEnum: FieldEnum = None) -> None: 50 | self.__delphiVersion = delphiVersion 51 | self.__fieldEnum = fieldEnum 52 | self.__tableAddr = addr 53 | self.__processorWordSize = GetProcessorWordSize() 54 | self.__typeName = "" 55 | 56 | if self.__tableAddr and self.__tableAddr != ida_idaapi.BADADDR: 57 | self.__typeName = GetStr_PASCAL(self.__tableAddr + 1) 58 | if self.__typeName is None: 59 | msg = ("TypeInfo: TypeName is None (" 60 | + hex(self.__tableAddr) 61 | + ").") 62 | raise DelphiHelperError(msg) 63 | 64 | global typeKindList 65 | self.__typeKind = Byte(self.__tableAddr) 66 | if self.__typeKind >= len(typeKindList): 67 | msg = ("TypeInfo: TypeKind out of range - " 68 | + str(self.__typeKind) 69 | + " (" 70 | + hex(self.__tableAddr) 71 | + ").") 72 | raise DelphiHelperError(msg) 73 | 74 | typeDataAddr = self.__tableAddr + 2 + Byte(self.__tableAddr + 1) 75 | 76 | if typeKindList[self.__typeKind] == "tkClass": 77 | self.__tkClass = TypeInfo_tkClass( 78 | typeDataAddr, 79 | self.__typeName, 80 | self.__delphiVersion 81 | ) 82 | elif typeKindList[self.__typeKind] == "tkRecord": 83 | self.__tkRecord = TypeInfo_tkRecord( 84 | typeDataAddr, 85 | self.__typeName, 86 | self.__delphiVersion 87 | ) 88 | 89 | def GetTableAddress(self) -> int: 90 | return self.__tableAddr 91 | 92 | def GetTypeName(self) -> str: 93 | return self.__typeName 94 | 95 | def MakeTable(self, resolveTypeInfoClass: int = 0) -> None: 96 | if self.__tableAddr and \ 97 | self.__tableAddr != ida_idaapi.BADADDR and \ 98 | ida_bytes.is_loaded(self.__tableAddr) and \ 99 | "_TypeInfo" not in ida_name.get_name(self.__tableAddr): 100 | print( 101 | f"[INFO] Processing {self.__typeName}_TypeInfo (0x{self.__tableAddr:X})" 102 | ) 103 | if resolveTypeInfoClass != 0: 104 | self.ResolveTypeInfo(self.__tableAddr) 105 | else: 106 | self.__DeleteTable() 107 | self.__CreateTable() 108 | self.__ExtractData() 109 | 110 | def ResolveTypeInfo( 111 | self, 112 | tableAddr: int, 113 | once: bool = False) -> None: 114 | if tableAddr == ida_idaapi.BADADDR or \ 115 | tableAddr == 0 or \ 116 | not ida_bytes.is_loaded(tableAddr): 117 | return 118 | 119 | if once: 120 | if "_TypeInfo" in ida_name.get_name(tableAddr) or \ 121 | not ida_bytes.is_loaded(tableAddr + self.__processorWordSize): 122 | return 123 | tableAddr += self.__processorWordSize 124 | 125 | typeKind = Byte(tableAddr) 126 | 127 | global typeKindList 128 | if typeKind != 0xff: 129 | if typeKindList[typeKind] == "tkClass": 130 | self.__ResolveTypeInfo_tkClass(tableAddr) 131 | else: 132 | typeInfo = TypeInfo( 133 | self.__delphiVersion, 134 | tableAddr 135 | ) 136 | typeInfo.MakeTable() 137 | 138 | def __ResolveTypeInfo_tkClass(self, tableAddr: int) -> None: 139 | if self.__processorWordSize == 4: 140 | ref = FindRef_Dword( 141 | tableAddr - 4, 142 | tableAddr, 143 | ida_bytes.BIN_SEARCH_BACKWARD 144 | ) 145 | else: 146 | ref = FindRef_Qword( 147 | tableAddr - 4, 148 | tableAddr, 149 | ida_bytes.BIN_SEARCH_BACKWARD 150 | ) 151 | 152 | if ref != ida_idaapi.BADADDR: 153 | classAddr = ref - 4 * self.__processorWordSize 154 | className = ida_name.get_name(classAddr) 155 | 156 | if not className.startswith("VMT_"): 157 | from DelphiHelper.core.DelphiClass import DelphiClass 158 | delphiClass = DelphiClass( 159 | classAddr, 160 | self.__delphiVersion 161 | ) 162 | delphiClass.MakeClass() 163 | 164 | def __CreateTableHeader(self) -> None: 165 | MakeByte(self.__tableAddr) 166 | 167 | global typeKindList 168 | if self.__typeKind < len(typeKindList): 169 | ida_bytes.set_cmt( 170 | self.__tableAddr, 171 | "Type kind - " + typeKindList[self.__typeKind], 172 | 0 173 | ) 174 | else: 175 | ida_bytes.set_cmt( 176 | self.__tableAddr, 177 | "Type kind - UNKNOWN", 178 | 0 179 | ) 180 | 181 | if Byte(self.__tableAddr + 1): 182 | MakeStr_PASCAL(self.__tableAddr + 1) 183 | ida_bytes.set_cmt(self.__tableAddr + 1, "Type name", 0) 184 | 185 | MakeName(self.__tableAddr, self.__typeName + "_TypeInfo") 186 | 187 | addr = GetCustomWord( 188 | self.__tableAddr - self.__processorWordSize, 189 | self.__processorWordSize 190 | ) 191 | 192 | if addr == self.__tableAddr: 193 | MakeCustomWord( 194 | self.__tableAddr - self.__processorWordSize, 195 | self.__processorWordSize 196 | ) 197 | MakeName( 198 | self.__tableAddr - self.__processorWordSize, 199 | "_" + self.__typeName + "_TypeInfo" 200 | ) 201 | 202 | def __CreateTable(self) -> None: 203 | self.__CreateTableHeader() 204 | 205 | global typeKindList 206 | if typeKindList[self.__typeKind] == "tkClass": 207 | self.__tkClass.CreateTypeData() 208 | elif typeKindList[self.__typeKind] == "tkRecord": 209 | self.__tkRecord.CreateTypeData() 210 | 211 | def __DeleteTableHeader(self) -> None: 212 | ida_bytes.del_items( 213 | self.__tableAddr, 214 | ida_bytes.DELIT_DELNAMES, 215 | 1 216 | ) 217 | ida_bytes.del_items( 218 | self.__tableAddr + 1, 219 | ida_bytes.DELIT_DELNAMES, 220 | 1 + Byte(self.__tableAddr + 1) 221 | ) 222 | 223 | addr = GetCustomWord( 224 | self.__tableAddr - self.__processorWordSize, 225 | self.__processorWordSize 226 | ) 227 | 228 | if addr == self.__tableAddr: 229 | ida_bytes.del_items( 230 | self.__tableAddr - self.__processorWordSize, 231 | ida_bytes.DELIT_DELNAMES, 232 | self.__processorWordSize 233 | ) 234 | 235 | def __DeleteTable(self) -> None: 236 | self.__DeleteTableHeader() 237 | 238 | global typeKindList 239 | if typeKindList[self.__typeKind] == "tkClass": 240 | self.__tkClass.DeleteTypeData() 241 | elif typeKindList[self.__typeKind] == "tkRecord": 242 | self.__tkRecord.DeleteTypeData() 243 | 244 | def __ExtractData(self) -> None: 245 | global typeKindList 246 | if typeKindList[self.__typeKind] == "tkClass": 247 | self.__tkClass.ExtractData_TypeData(self.__fieldEnum) 248 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_FieldTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's FieldTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import ida_name 11 | import idc 12 | from DelphiHelper.core.DelphiClass_ClassTable import * 13 | from DelphiHelper.core.DelphiClass_TypeInfo import * 14 | from DelphiHelper.util.ida import * 15 | from typing import Optional 16 | 17 | 18 | class FieldTable(object): 19 | 20 | def __init__( 21 | self, 22 | delphiVersion: int, 23 | classInfo: dict[str, str | dict[str, int]], 24 | fieldEnum: FieldEnum) -> None: 25 | self.__tableAddr = classInfo["Address"]["FieldTable"] 26 | self.__classInfo = classInfo 27 | self.__fieldEnum = fieldEnum 28 | self.__tableName = classInfo["Name"] 29 | self.__NoNameCounter = 1 30 | self.__processorWordSize = GetProcessorWordSize() 31 | self.__classTableEntries = list() 32 | self.__delphiVersion = delphiVersion 33 | 34 | if self.__tableAddr != 0: 35 | self.__classTableAddr = GetCustomWord( 36 | self.__tableAddr + 2, 37 | self.__processorWordSize 38 | ) 39 | 40 | def GetTableAddress(self) -> int: 41 | return self.__tableAddr 42 | 43 | def MakeTable(self) -> None: 44 | if self.__tableAddr != 0: 45 | self.__DeleteTable() 46 | self.__CreateTableAndExtractData() 47 | 48 | def __CreateExtendedTableAndExtractData(self, addr: int) -> None: 49 | MakeWord(addr) 50 | numOfEntries = Word(addr) 51 | MakeName(addr, self.__tableName + "_ExtendedFieldTable") 52 | ida_bytes.set_cmt(addr, "Number of records", 0) 53 | 54 | addr += 2 55 | 56 | for i in range(numOfEntries): 57 | nameAddr = addr + 5 + self.__processorWordSize 58 | recordSize = (8 + self.__processorWordSize 59 | + Byte(nameAddr) 60 | + Word(addr + 6 61 | + self.__processorWordSize 62 | + Byte(nameAddr)) 63 | - 2) 64 | 65 | MakeByte(addr) 66 | 67 | MakeCustomWord(addr + 1, self.__processorWordSize) 68 | typeInfoAddr = GetCustomWord(addr + 1, self.__processorWordSize) 69 | typeName = self.__ExtractTypeName(typeInfoAddr) 70 | 71 | if typeName is not None: 72 | MakeDword(addr + 1 + self.__processorWordSize) 73 | offset = Dword(addr + 1 + self.__processorWordSize) 74 | 75 | if Byte(nameAddr) != 0: 76 | MakeStr_PASCAL(nameAddr) 77 | name = GetStr_PASCAL(nameAddr) 78 | else: 79 | MakeByte(nameAddr) 80 | name = "NoName" + str(self.__NoNameCounter) 81 | self.__NoNameCounter += 1 82 | 83 | MakeWord(addr + 6 84 | + self.__processorWordSize 85 | + Byte(nameAddr)) 86 | 87 | if name[0] == "F": 88 | tempName = name[1:] 89 | else: 90 | tempName = name 91 | 92 | self.__fieldEnum.AddMember(typeName, tempName, offset) 93 | addr = addr + recordSize 94 | else: 95 | return 96 | 97 | def __CreateTableAndExtractData(self) -> None: 98 | MakeWord(self.__tableAddr) 99 | MakeName(self.__tableAddr, self.__tableName + "_FieldTable") 100 | 101 | ida_bytes.set_cmt( 102 | self.__tableAddr, 103 | "Number of records", 104 | 0 105 | ) 106 | 107 | MakeCustomWord(self.__tableAddr + 2, self.__processorWordSize) 108 | 109 | ida_bytes.set_cmt( 110 | self.__tableAddr + 2, 111 | "Class table", 112 | 0 113 | ) 114 | 115 | classTable = ClassTable( 116 | self.__classTableAddr, 117 | self.__tableName, 118 | self.__delphiVersion 119 | ) 120 | self.__classTableEntries = classTable.MakeTable() 121 | 122 | addr = self.__tableAddr + 2 + self.__processorWordSize 123 | numOfEntries = Word(self.__tableAddr) 124 | 125 | for i in range(numOfEntries): 126 | fieldClassInfo = None 127 | recordSize = 7 + Byte(addr + 6) 128 | 129 | MakeDword(addr) 130 | offset = Dword(addr) 131 | MakeWord(addr + 4) 132 | index = Word(addr + 4) 133 | 134 | if Byte(addr + 6) != 0: 135 | MakeStr_PASCAL(addr + 6) 136 | name = GetStr_PASCAL(addr + 6) 137 | else: 138 | MakeByte(addr + 6) 139 | name = "NoName" + str(self.__NoNameCounter) 140 | self.__NoNameCounter += 1 141 | 142 | if ida_bytes.is_loaded(self.__classTableEntries[index]) and \ 143 | self.__classTableEntries[index] != 0: 144 | from DelphiHelper.core.DelphiClass import DelphiClass 145 | delphiClass = DelphiClass( 146 | self.__classTableEntries[index], 147 | self.__delphiVersion 148 | ) 149 | fieldClassInfo = delphiClass.GetClassInfo() 150 | 151 | ida_bytes.set_cmt( 152 | addr, 153 | fieldClassInfo["FullName"], 154 | 0 155 | ) 156 | 157 | MakeName(addr, self.__tableName + "_" + name) 158 | 159 | if name[0] == "F": 160 | tempName = name[1:] 161 | else: 162 | tempName = name 163 | 164 | if fieldClassInfo is None: 165 | self.__fieldEnum.AddMember( 166 | "Unknown", 167 | tempName, 168 | offset 169 | ) 170 | else: 171 | self.__fieldEnum.AddMember( 172 | fieldClassInfo["Name"], 173 | tempName, 174 | offset 175 | ) 176 | 177 | addr = addr + recordSize 178 | 179 | methodTableAddr = self.__classInfo["Address"]["MethodTable"] 180 | dynamicTableAddr = self.__classInfo["Address"]["DynamicTable"] 181 | classNameAddr = self.__classInfo["Address"]["ClassName"] 182 | 183 | if (methodTableAddr != 0 and addr < methodTableAddr) or \ 184 | (methodTableAddr == 0 and dynamicTableAddr != 0 and addr < dynamicTableAddr) or \ 185 | (methodTableAddr == 0 and dynamicTableAddr == 0 and addr < classNameAddr): 186 | self.__CreateExtendedTableAndExtractData(addr) 187 | 188 | def __DeleteTable(self) -> None: 189 | ida_bytes.del_items( 190 | self.__tableAddr, 191 | ida_bytes.DELIT_DELNAMES, 192 | 2 + self.__processorWordSize 193 | ) 194 | 195 | addr = self.__tableAddr + 2 + self.__processorWordSize 196 | numOfEntries = Word(self.__tableAddr) 197 | 198 | for i in range(numOfEntries): 199 | recordSize = 7 + Byte(addr + 6) 200 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, recordSize) 201 | addr = addr + recordSize 202 | 203 | methodTableAddr = self.__classInfo["Address"]["MethodTable"] 204 | dynamicTableAddr = self.__classInfo["Address"]["DynamicTable"] 205 | classNameAddr = self.__classInfo["Address"]["ClassName"] 206 | 207 | if (methodTableAddr != 0 and addr < methodTableAddr) or \ 208 | (methodTableAddr == 0 and dynamicTableAddr != 0 and addr < dynamicTableAddr) or \ 209 | (methodTableAddr == 0 and dynamicTableAddr == 0 and addr < classNameAddr): 210 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, 2) 211 | numOfEntries = Word(addr) 212 | addr += 2 213 | 214 | for i in range(numOfEntries): 215 | recordSize = (8 + self.__processorWordSize 216 | + Byte(addr + 5 + self.__processorWordSize) 217 | + Word(addr + 6 + self.__processorWordSize + Byte(addr + 5 + self.__processorWordSize)) 218 | - 2) 219 | 220 | ida_bytes.del_items( 221 | addr, 222 | ida_bytes.DELIT_DELNAMES, 223 | recordSize 224 | ) 225 | 226 | addr = addr + recordSize 227 | 228 | def __ExtractTypeName(self, addr: int) -> Optional[str]: 229 | typeName = None 230 | 231 | if addr == 0: 232 | typeName = "NoType" 233 | elif ida_bytes.is_loaded(addr): 234 | typeInfo = TypeInfo( 235 | self.__delphiVersion, 236 | addr + self.__processorWordSize 237 | ) 238 | typeInfo.MakeTable(1) 239 | typeName = typeInfo.GetTypeName() 240 | elif idc.get_segm_name(addr) == ".idata" and ida_name.get_name(addr).startswith("@"): 241 | typeName = ida_name.get_name(addr) 242 | typeName = typeName.split('@')[-1] 243 | typeName = typeName.split('$')[-1] 244 | typeName = "".join([i for i in typeName if not i.isdigit()]) 245 | 246 | if not len(typeName): 247 | typeName = None 248 | 249 | return typeName 250 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_MethodTable.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse Delphi's MethodTable 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | import ida_bytes 9 | import ida_name 10 | import idc 11 | from DelphiHelper.core.DelphiClass_TypeInfo import * 12 | from DelphiHelper.util.delphi import GetParamRegister 13 | from DelphiHelper.util.ida import * 14 | 15 | 16 | class MethodTable(object): 17 | 18 | def __init__( 19 | self, 20 | delphiVersion: int, 21 | classInfo: dict[str, str | dict[str, int]]) -> None: 22 | self.__tableAddr = classInfo["Address"]["MethodTable"] 23 | self.__tableName = classInfo["Name"] 24 | self.__classInfo = classInfo 25 | self.__processorWordSize = GetProcessorWordSize() 26 | self.__delphiVersion = delphiVersion 27 | 28 | def GetTableAddress(self) -> int: 29 | return self.__tableAddr 30 | 31 | def MakeTable(self) -> None: 32 | if self.__tableAddr != 0: 33 | self.__DeleteTable() 34 | self.__CreateTable() 35 | 36 | def GetMethods(self) -> list[tuple[str, int]]: 37 | methodList = list() 38 | 39 | if self.__tableAddr != 0: 40 | numOfEntries = Word(self.__tableAddr) 41 | addr = self.__tableAddr + 2 42 | 43 | for i in range(numOfEntries): 44 | methodAddr = GetCustomWord(addr + 2, self.__processorWordSize) 45 | methodName = GetStr_PASCAL(addr + 2 + self.__processorWordSize) 46 | addr += Word(addr) 47 | methodList.append((methodName, methodAddr)) 48 | 49 | return methodList 50 | 51 | def __CreateTable(self) -> None: 52 | MakeWord(self.__tableAddr) 53 | MakeName(self.__tableAddr, self.__tableName + "_MethodTable") 54 | 55 | ida_bytes.set_cmt( 56 | self.__tableAddr, 57 | "Number of records", 58 | 0 59 | ) 60 | 61 | numOfEntries = Word(self.__tableAddr) 62 | if numOfEntries != 0: 63 | ida_bytes.set_cmt( 64 | self.__tableAddr + 2, 65 | "Record size", 66 | 0 67 | ) 68 | ida_bytes.set_cmt( 69 | self.__tableAddr + 4, 70 | "Method pointer", 71 | 0 72 | ) 73 | ida_bytes.set_cmt( 74 | self.__tableAddr + 4 + self.__processorWordSize, 75 | "Method Name", 76 | 0 77 | ) 78 | 79 | addr = self.__tableAddr + 2 80 | for i in range(numOfEntries): 81 | recordSize = Word(addr) 82 | 83 | MakeWord(addr) 84 | MakeFunction(GetCustomWord(addr + 2, self.__processorWordSize)) 85 | MakeCustomWord(addr + 2, self.__processorWordSize) 86 | MakeStr_PASCAL(addr + 2 + self.__processorWordSize) 87 | 88 | name = (self.__tableName 89 | + "_" 90 | + GetStr_PASCAL(addr + 2 + self.__processorWordSize)) 91 | 92 | MakeName(GetCustomWord(addr + 2, self.__processorWordSize), name) 93 | 94 | addr = addr + recordSize 95 | 96 | dynamicTableAddr = self.__classInfo["Address"]["DynamicTable"] 97 | classNameAddr = self.__classInfo["Address"]["ClassName"] 98 | 99 | if (dynamicTableAddr == 0 and addr < classNameAddr) or \ 100 | (dynamicTableAddr != 0 and addr < dynamicTableAddr): 101 | numOfEntries = Word(addr) 102 | MakeWord(addr) 103 | addr += 2 104 | 105 | for i in range(numOfEntries): 106 | MakeCustomWord(addr, self.__processorWordSize) 107 | MakeByte(addr + self.__processorWordSize) 108 | idc.make_array(addr + self.__processorWordSize, 4) 109 | 110 | recordAddr = GetCustomWord(addr, self.__processorWordSize) 111 | self.__CreateFunctionRecord(recordAddr) 112 | addr += 4 + self.__processorWordSize 113 | 114 | def __CreateFunctionRecord(self, addr: int) -> None: 115 | recordSize = Word(addr) 116 | funcNameAddr = addr + 2 117 | 118 | MakeWord(addr) 119 | 120 | nameAddr = GetCustomWord(funcNameAddr, self.__processorWordSize) 121 | name = ida_name.get_name(nameAddr) 122 | if self.__tableName not in name: 123 | MakeFunction(GetCustomWord(funcNameAddr, self.__processorWordSize)) 124 | 125 | MakeCustomWord(funcNameAddr, self.__processorWordSize) 126 | MakeStr_PASCAL(funcNameAddr + self.__processorWordSize) 127 | funcBaseName = GetStr_PASCAL(funcNameAddr + self.__processorWordSize) 128 | MakeName(addr, "_" + self.__tableName + "_" + funcBaseName) 129 | 130 | funcPrototype = ("void __usercall " 131 | + self.__tableName 132 | + "_" 133 | + funcBaseName 134 | + "(") 135 | 136 | size = (3 + self.__processorWordSize 137 | + Byte(funcNameAddr + self.__processorWordSize)) 138 | 139 | if recordSize > size: 140 | addr += size 141 | MakeCustomWord(addr, self.__processorWordSize) 142 | addr += self.__processorWordSize 143 | MakeDword(addr) 144 | addr += 4 145 | MakeByte(addr) 146 | 147 | numOfParams = Byte(addr) 148 | addr += 1 149 | 150 | if funcBaseName == "Create": 151 | numOfParams += 1 152 | 153 | for i in range(numOfParams): 154 | regStr = GetParamRegister(i) 155 | 156 | if i == 1 and funcBaseName == "Create": 157 | funcPrototype += "void* ShortInt_Alloc" + regStr 158 | else: 159 | MakeByte(addr) 160 | MakeCustomWord(addr + 1, self.__processorWordSize) 161 | MakeWord(addr + 1 + self.__processorWordSize) 162 | MakeStr_PASCAL(addr + 3 + self.__processorWordSize) 163 | 164 | wordAddr = (addr + 4 165 | + self.__processorWordSize 166 | + Byte(addr + 3 + self.__processorWordSize)) 167 | MakeWord(wordAddr) 168 | 169 | argTypeInfo = GetCustomWord( 170 | addr + 1, 171 | self.__processorWordSize 172 | ) 173 | 174 | if argTypeInfo == 0: 175 | typeName = "NoType" 176 | elif ida_bytes.is_mapped(argTypeInfo) and ida_bytes.is_loaded(argTypeInfo): 177 | typeInfoAddr = argTypeInfo + self.__processorWordSize 178 | typeInfo = TypeInfo( 179 | self.__delphiVersion, 180 | typeInfoAddr 181 | ) 182 | typeInfo.MakeTable(1) 183 | typeName = typeInfo.GetTypeName() 184 | else: 185 | return 186 | 187 | paramNameAddr = addr + 3 + self.__processorWordSize 188 | paramName = GetStr_PASCAL(paramNameAddr) 189 | if paramName is None: 190 | paramName = "RetVal" 191 | 192 | funcPrototype += ("void* " 193 | + typeName 194 | + "_" 195 | + paramName 196 | + regStr) 197 | 198 | addr = (addr + 6 199 | + self.__processorWordSize 200 | + Byte(addr + 3 + self.__processorWordSize)) 201 | 202 | if i != numOfParams - 1: 203 | funcPrototype += ", " 204 | 205 | MakeWord(addr) 206 | 207 | funcPrototype += ");" 208 | 209 | nameAddr = GetCustomWord(funcNameAddr, self.__processorWordSize) 210 | name = ida_name.get_name(nameAddr) 211 | 212 | if Byte(nameAddr) == 0: 213 | MakeName(nameAddr, "sub_nullsub") 214 | else: 215 | if self.__tableName not in name: 216 | MakeName(nameAddr, self.__tableName + "_" + funcBaseName) 217 | 218 | idc.SetType(nameAddr, funcPrototype) 219 | 220 | def __DeleteTable(self) -> None: 221 | ida_bytes.del_items(self.__tableAddr, ida_bytes.DELIT_DELNAMES, 2) 222 | 223 | addr = self.__tableAddr + 2 224 | numOfEntries = Word(self.__tableAddr) 225 | 226 | for i in range(numOfEntries): 227 | recordSize = Word(addr) 228 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, recordSize) 229 | addr = addr + recordSize 230 | 231 | dynamicTableAddr = self.__classInfo["Address"]["DynamicTable"] 232 | classNameAddr = self.__classInfo["Address"]["ClassName"] 233 | 234 | if (dynamicTableAddr == 0 and addr < classNameAddr) or \ 235 | (dynamicTableAddr != 0 and addr < dynamicTableAddr): 236 | numOfEntries = Word(addr) 237 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, 2) 238 | addr += 2 239 | 240 | for i in range(numOfEntries): 241 | ida_bytes.del_items( 242 | addr, 243 | ida_bytes.DELIT_DELNAMES, 244 | 4 + self.__processorWordSize 245 | ) 246 | ida_bytes.del_items( 247 | GetCustomWord(addr, self.__processorWordSize), 248 | ida_bytes.DELIT_DELNAMES, 249 | Word(GetCustomWord(addr, self.__processorWordSize)) 250 | ) 251 | addr += 4 + self.__processorWordSize 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DelphiHelper 2 | 3 | DelphiHelper is a python IDA Pro plugin aiming to help the analysis of 4 | x86/x86_64 binaries written in 5 | [Delphi](https://www.embarcadero.com/products/delphi) programming language. 6 | 7 | ## Table of Contents 8 | 9 | 10 | ## Features 11 | 12 | * Displays an interactive tree view of Delphi's DFM (Delphi Form Modules) 13 | resource (Delphi Form description) 14 | * Parses Delphi's RTTI data structures 15 | * Extracts useful information from Delphi's RTTI data structures 16 | * Extracts binary files embedded in Delphi's DFM resources 17 | * Searches for the Entry Point (EP) function 18 | * Loads Delphi's FLIRT signatures automatically 19 | * Loads Interactive Delphi Reconstructor (IDR) Knowledge Base (KB) function 20 | signatures 21 | 22 | ## Installation 23 | 24 | 1. Copy `DelphiHelper` directory and `DelphiHelper.py` inside the IDA plugin 25 | directory: 26 | * on Windows: `%APPDATA%\Hex-Rays\IDA Pro\plugins\` 27 | * on macOS/Linux: `$HOME/.idapro/plugins/` 28 | 2. Download IDR Knowledge Base files manually by following 29 | [instructions](#download-instructions) given below or run the script 30 | `setup_IDRKB.py`. 31 | 32 | > **_NOTE_**: `py7zr` installation is required to run the setup script: 33 | `pip install py7zr` 34 | 35 | **DelphiHelper** should be compatible with IDA 8.4 and later on Windows, macOS 36 | and GNU/Linux. 37 | 38 | ## Usage 39 | 40 | In order to use the DelphiHelper plugin, the resource section has to be loaded 41 | in IDA. This can be achieved by checking the box **Load resources** or with the 42 | option `-R` in the command line switch. 43 | 44 | ![Loading a file in IDA Pro GUI.](img/LoadFile.PNG) 45 | 46 | > **_NOTE_**: IDA autoanalysis must be completed before using the plugin. 47 | 48 | ### Hotkeys 49 | 50 | * ``: Print Help 51 | * ``: Run [VMT Parser](#vmt-parser) 52 | * ``: Run [DFM Finder](#dfm-finder) (show Delphi Form Viewer window) 53 | * ``: Run [Entry Point Function Finder](#entry-point-function-finder) 54 | * ``: Run [IDR Knowledge Base Loader](#idr-knowledge-base-loader) for `SysInit` and `System` unit 55 | * ``: Run [IDR Knowledge Base Loader](#idr-knowledge-base-loader) 56 | for selected units 57 | 58 | ### VMT Parser 59 | 60 | **Hotkey**: `` 61 | 62 | The **Virtual Method Table (VMT) Parser** must be executed with the cursor on 63 | the starting address of a VMT structure. This structure can be located by 64 | searching the functions named `CreateForm`, `CreateObject`, etc.; once the 65 | function is found, the last argument should be the VMT structure. 66 | 67 | In most cases there is a sequence of two instructions: 68 | ``` 69 | mov , 70 | call CreateForm 71 | ``` 72 | 73 | For example: 74 | 75 | ![Identifying of VMT structure offset.](img/CreateForm.PNG) 76 | 77 | Once on the structure (VMTStructureOffset e.g. `off_664A8C`), press ``: 78 | 79 | ![VMT structure.](img/VMTStructure.PNG) 80 | 81 | Press the hotkey ``. The result looks like below: 82 | 83 | ![Parsed VMT structure.](img/VMTStructureParsed.PNG) 84 | 85 | The VMT structure contains a lot of useful information stored in various RTTI 86 | tables. The most interesting ones are: 87 | * [Field Table](#field-table) 88 | * [Virtual Method Table (VMT)](#virtual-method-table-(vmt)) 89 | * [Method Table](#method-table) 90 | 91 | The **VMT Parser** automatically extracts data from those tables and stores it 92 | into IDA enums and structures. 93 | 94 | #### Field Table 95 | 96 | The Field Table stores a name, a type and an offset of each published field. 97 | The **VMT Parser** extracts and saves all these entries for each VMT structure 98 | into IDA Enums: 99 | 100 | ![Field Table](img/FieldTable.PNG) 101 | 102 | The enums created by the **VMT Parser** have following format: 103 | `%ObjectName%_Fields`. The names of enum entries have this format: 104 | `%ObjectName%_%FieldType%_%FieldName%`: 105 | 106 | ![Field Table in IDA Enum](img/IDAEnum_FieldTable.PNG) 107 | 108 | The entries from enums created by the **VMT Parser** can be then named by 109 | pressing built-in IDA shortcut ``, used for naming symbolic constants. 110 | 111 | **Before** 112 | 113 | ![Resolving a field from Field Table](img/FieldInFunc_Before.PNG) 114 | 115 | **After** 116 | 117 | ![Resolving a field from Field Table](img/FieldInFunc_After.PNG) 118 | 119 | #### Virtual Method Table (VMT) 120 | 121 | The Virtual Method Table stores pointers to all virtual methods declared for a 122 | class and its base classes: 123 | 124 | ![VMT table](img/VMT.PNG) 125 | 126 | The **VMT Parser** extracts pointers from the VMT and saves them into an IDA 127 | structure named by the parsed VMT structure. The names of structure entries 128 | have the following format: `%MethodName%_%MethodOffset%`: 129 | 130 | ![VMT table in IDA Structure](img/IDAStruct_VMT.PNG) 131 | 132 | The entries from structures created by the **VMT Parser** can be then named by 133 | pressing the built-in IDA shortcut ``, used for naming structure offsets. 134 | 135 | **Before** 136 | 137 | ![Call VMT method by offset](img/CallVMTFunc_Before.PNG) 138 | 139 | **After** 140 | 141 | ![Call VMT method by offset](img/CallVMTFunc_After.PNG) 142 | 143 | 144 | #### Method Table 145 | 146 | The Method Table stores names and pointers of published methods. The Method 147 | Table also contains pointers of Delphi Event handlers that can be found and 148 | accessed from the **Delphi Form Viewer** (for more info see [DFM 149 | Finder](#dfm-finder)) 150 | 151 | ![Method Table](img/MethodTable.PNG) 152 | 153 | ### DFM Finder 154 | 155 | **Hotkey**: `` 185 | 186 | **Entry Point Function Finder** tries to find possible entry point functions by 187 | searching for references to `CreateFrom`, `InitExe` and `InitLib` calls. 188 | Results are presented in IDA's output window, e.g.: 189 | 190 | ![Ouput of EP Function Finder](img/EPFinder.PNG) 191 | 192 | ### IDR Knowledge Base Loader 193 | 194 | **Hotkeys**: `` and `` 195 | 196 | **IDR Knowledge Base Loader** loads IDR KB signatures from KB files. The plugin 197 | expects to have the KB files stored in the following location: 198 | 199 | * on Windows: `%APPDATA%\Hex-Rays\IDA Pro\plugins\DelphiHelper\IDR_KB\` 200 | 201 | * on macOS/Linux: `$HOME/.idapro/plugins/DelphiHelper/IDR_KB/` 202 | 203 | `` loads function signatures for only `SysInit` and `System`. 204 | 205 | `` tries to load function signatures for all units selected from a 206 | list of imported units. 207 | 208 | > **_NOTE_**: The KB files are downloaded from the IDR project:
209 | > https://github.com/crypto2011/IDR
210 | > https://github.com/crypto2011/IDR64 211 | 212 | The IDR project is licensed under the MIT license: 213 | ``` 214 | MIT License 215 | 216 | Copyright (c) 2006-2018 crypto 217 | 218 | Permission is hereby granted, free of charge, to any person obtaining a copy 219 | of this software and associated documentation files (the "Software"), to deal 220 | in the Software without restriction, including without limitation the rights 221 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 222 | copies of the Software, and to permit persons to whom the Software is 223 | furnished to do so, subject to the following conditions: 224 | 225 | The above copyright notice and this permission notice shall be included in all 226 | copies or substantial portions of the Software. 227 | 228 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 229 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 230 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 231 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 232 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 233 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 234 | SOFTWARE. 235 | ``` 236 | 237 | #### Download instructions 238 | 239 | Download and copy following KB files into `DelphiHelper\IDR_KB\IDR64\` 240 | directory:
241 | https://github.com/crypto2011/IDR64/blob/master/syskb2012.bin
242 | https://github.com/crypto2011/IDR64/blob/master/syskb2013.bin
243 | https://github.com/crypto2011/IDR64/blob/master/syskb2014.bin
244 | 245 | Download and **extract** following KB files into `DelphiHelper\IDR_KB\IDR\` 246 | directory:
247 | https://github.com/crypto2011/IDR/blob/master/kb2005.7z
248 | https://github.com/crypto2011/IDR/blob/master/kb2006.7z
249 | https://github.com/crypto2011/IDR/blob/master/kb2007.7z
250 | https://github.com/crypto2011/IDR/blob/master/kb2009.7z
251 | https://github.com/crypto2011/IDR/blob/master/kb2010.7z
252 | https://github.com/crypto2011/IDR/blob/master/kb2011.7z
253 | https://github.com/crypto2011/IDR/blob/master/kb2012.7z
254 | https://github.com/crypto2011/IDR/blob/master/kb2013.7z
255 | https://github.com/crypto2011/IDR/blob/master/kb2014.7z 256 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements class for storing data extracted from Delphi's VMT 3 | # structure 4 | # 5 | # Copyright (c) 2020-2025 ESET 6 | # Author: Juraj Horňák 7 | # See LICENSE file for redistribution. 8 | 9 | 10 | import ida_bytes 11 | import ida_idaapi 12 | import ida_name 13 | from DelphiHelper.core.DelphiClass_DynamicTable import * 14 | from DelphiHelper.core.DelphiClass_FieldTable import * 15 | from DelphiHelper.core.DelphiClass_InitTable import * 16 | from DelphiHelper.core.DelphiClass_IntfTable import * 17 | from DelphiHelper.core.DelphiClass_MethodTable import * 18 | from DelphiHelper.core.DelphiClass_TypeInfo import * 19 | from DelphiHelper.core.DelphiClass_VMTTable import * 20 | from DelphiHelper.core.FieldEnum import * 21 | from DelphiHelper.core.FuncStruct import * 22 | from DelphiHelper.util.delphi import DemangleFuncName 23 | from DelphiHelper.util.exception import DelphiHelperError 24 | from DelphiHelper.util.ida import * 25 | 26 | 27 | class DelphiClass(object): 28 | __CLASS_DESCRIPTION = [ 29 | "SelfPtr", 30 | "IntfTable", 31 | "AutoTable", 32 | "InitTable", 33 | "TypeInfo", 34 | "FieldTable", 35 | "MethodTable", 36 | "DynamicTable", 37 | "ClassName", 38 | "InstanceSize", 39 | "Parent" 40 | ] 41 | 42 | def __init__( 43 | self, 44 | VMT_addr: int, 45 | delphiVersion: int, 46 | className: str = str()) -> None: 47 | self.__processorWordSize = GetProcessorWordSize() 48 | self.__delphiVersion = delphiVersion 49 | 50 | if VMT_addr == 0: 51 | self.__VMTaddr = self.__GetVMTAddrByName(className) 52 | else: 53 | self.__VMTaddr = VMT_addr 54 | 55 | if self.IsDelphiClass(): 56 | self.__classInfo = self.GetClassInfo() 57 | 58 | self.__fieldEnum = FieldEnum( 59 | self.__classInfo["Name"], 60 | self.__classInfo["FullName"] 61 | ) 62 | 63 | self.__funcStruct = FuncStruct( 64 | self.__classInfo["Name"], 65 | self.__classInfo["FullName"] 66 | ) 67 | 68 | self.__intfTable = IntfTable( 69 | self.__delphiVersion, 70 | self.__classInfo 71 | ) 72 | 73 | self.__initTable = InitTable( 74 | self.__delphiVersion, 75 | self.__classInfo, 76 | self.__fieldEnum, 77 | ) 78 | 79 | self.__typeInfo = TypeInfo( 80 | self.__delphiVersion, 81 | self.__classInfo["Address"]["TypeInfo"], 82 | self.__fieldEnum 83 | ) 84 | 85 | self.__fieldTable = FieldTable( 86 | self.__delphiVersion, 87 | self.__classInfo, 88 | self.__fieldEnum 89 | ) 90 | 91 | self.__methodTable = MethodTable( 92 | self.__delphiVersion, 93 | self.__classInfo 94 | ) 95 | self.__dynamicTable = DynamicTable(self.__classInfo) 96 | self.__VMTTable = VMTTable(self.__classInfo, self.__funcStruct) 97 | else: 98 | raise DelphiHelperError("Invalid VMT structure address: " + hex(self.__VMTaddr)) 99 | 100 | def GetClassInfo(self) -> dict[str, str | dict[str, int]]: 101 | classInfo = {} 102 | classInfo["Address"] = self.__GetAddressTable() 103 | classInfo["Name"] = self.__GetClassName() 104 | classInfo["FullName"] = self.__GetVMTClassName() 105 | return classInfo 106 | 107 | def GetVMTAddress(self) -> int: 108 | return self.__VMTaddr 109 | 110 | def GetClassName(self) -> str: 111 | return self.__classInfo["Name"] 112 | 113 | def GetClassFullName(self) -> str: 114 | return self.__classInfo["FullName"] 115 | 116 | def GetClassAddress(self) -> int: 117 | return self.__classInfo["Address"]["Class"] 118 | 119 | def GetMethods(self) -> list[tuple[str, int]]: 120 | return self.__methodTable.GetMethods() 121 | 122 | def MakeClass(self) -> None: 123 | print(f"[INFO] Processing {self.__classInfo['FullName']}") 124 | 125 | self.__DeleteClassHeader() 126 | self.__MakeClassName() 127 | 128 | self.__ResolveParent(self.__classInfo["Address"]["ParentClass"]) 129 | 130 | self.__intfTable.MakeTable() 131 | self.__initTable.MakeTable() 132 | self.__typeInfo.MakeTable() 133 | self.__fieldTable.MakeTable() 134 | self.__methodTable.MakeTable() 135 | self.__dynamicTable.MakeTable() 136 | self.__VMTTable.MakeTable() 137 | 138 | self.__MakeClassHeader() 139 | 140 | def IsDelphiClass(self) -> bool: 141 | if not ida_bytes.is_loaded(self.__VMTaddr) or \ 142 | self.__VMTaddr == ida_idaapi.BADADDR or \ 143 | self.__VMTaddr == 0: 144 | return False 145 | 146 | vmtTableAddr = GetCustomWord(self.__VMTaddr, self.__processorWordSize) 147 | 148 | if vmtTableAddr == 0 or vmtTableAddr < self.__VMTaddr: 149 | return False 150 | 151 | offset = vmtTableAddr - self.__VMTaddr 152 | 153 | if offset % self.__processorWordSize != 0 or \ 154 | offset / self.__processorWordSize > 30 or \ 155 | offset / self.__processorWordSize < 5: 156 | return False 157 | 158 | return True 159 | 160 | def __GetVMTClassName(self) -> str: 161 | return ("VMT_" 162 | + ("%x" % self.__VMTaddr).upper() 163 | + "_" 164 | + self.__GetClassName()) 165 | 166 | def __GetClassName(self) -> str: 167 | return FixName(GetStr_PASCAL(self.__GetClassNameAddr())) 168 | 169 | def __GetClassNameAddr(self) -> int: 170 | return GetCustomWord( 171 | self.__VMTaddr + 8 * self.__processorWordSize, 172 | self.__processorWordSize 173 | ) 174 | 175 | def __GetIntfTableAddr(self) -> int: 176 | return GetCustomWord( 177 | self.__VMTaddr + 1 * self.__processorWordSize, 178 | self.__processorWordSize 179 | ) 180 | 181 | def __GetAutoTableAddr(self) -> int: 182 | return GetCustomWord( 183 | self.__VMTaddr + 2 * self.__processorWordSize, 184 | self.__processorWordSize 185 | ) 186 | 187 | def __GetInitTableAddr(self) -> int: 188 | return GetCustomWord( 189 | self.__VMTaddr + 3 * self.__processorWordSize, 190 | self.__processorWordSize 191 | ) 192 | 193 | def __GetTypeInfoAddr(self) -> int: 194 | return GetCustomWord( 195 | self.__VMTaddr + 4 * self.__processorWordSize, 196 | self.__processorWordSize 197 | ) 198 | 199 | def __GetFieldTableAddr(self) -> int: 200 | return GetCustomWord( 201 | self.__VMTaddr + 5 * self.__processorWordSize, 202 | self.__processorWordSize 203 | ) 204 | 205 | def __GetMethodTableAddr(self) -> int: 206 | return GetCustomWord( 207 | self.__VMTaddr + 6 * self.__processorWordSize, 208 | self.__processorWordSize 209 | ) 210 | 211 | def __GetDynamicTableAddr(self) -> int: 212 | return GetCustomWord( 213 | self.__VMTaddr + 7 * self.__processorWordSize, 214 | self.__processorWordSize 215 | ) 216 | 217 | def __GetParentClassAddr(self) -> int: 218 | return GetCustomWord( 219 | self.__VMTaddr + 10 * self.__processorWordSize, 220 | self.__processorWordSize 221 | ) 222 | 223 | def __GetAddressTable(self) -> dict[str, int]: 224 | addressTable = {} 225 | addressTable["Class"] = self.__VMTaddr 226 | addressTable["VMTTable"] = GetCustomWord( 227 | self.__VMTaddr, 228 | self.__processorWordSize 229 | ) 230 | addressTable["ParentClass"] = self.__GetParentClassAddr() 231 | addressTable["IntfTable"] = self.__GetIntfTableAddr() 232 | addressTable["AutoTable"] = self.__GetAutoTableAddr() 233 | addressTable["InitTable"] = self.__GetInitTableAddr() 234 | addressTable["TypeInfo"] = self.__GetTypeInfoAddr() 235 | addressTable["FieldTable"] = self.__GetFieldTableAddr() 236 | addressTable["MethodTable"] = self.__GetMethodTableAddr() 237 | addressTable["DynamicTable"] = self.__GetDynamicTableAddr() 238 | addressTable["ClassName"] = self.__GetClassNameAddr() 239 | return addressTable 240 | 241 | def __GetVMTAddrByName(self, className: str) -> int: 242 | stringToFind = " " 243 | 244 | if className == str(): 245 | return 0 246 | 247 | for a in className: 248 | stringToFind += hex(ord(a))[2:] + " " 249 | 250 | stringToFind = "07 " + hex(len(className))[2:] + stringToFind 251 | 252 | addr = find_bytes(stringToFind) 253 | if addr != ida_idaapi.BADADDR: 254 | addr += 2 + len(className) 255 | addr = FindRef_Dword( 256 | GetCustomWord(addr, 4), 257 | GetCustomWord(addr, 4), 258 | ida_bytes.BIN_SEARCH_BACKWARD 259 | ) 260 | 261 | return addr 262 | 263 | def __DeleteClassHeader(self) -> None: 264 | ida_bytes.del_items( 265 | self.__VMTaddr, 266 | ida_bytes.DELIT_DELNAMES, 267 | GetCustomWord( 268 | self.__VMTaddr, 269 | self.__processorWordSize 270 | ) - self.__VMTaddr 271 | ) 272 | 273 | def __MakeClassHeader(self) -> None: 274 | addr = self.__VMTaddr 275 | endAddr = GetCustomWord(self.__VMTaddr, self.__processorWordSize) 276 | i = 0 277 | 278 | while addr < endAddr and i < 30: 279 | MakeCustomWord(addr, self.__processorWordSize) 280 | 281 | if addr < self.__VMTaddr + 11 * self.__processorWordSize: 282 | ida_bytes.set_cmt(addr, self.__CLASS_DESCRIPTION[i], 0) 283 | else: 284 | DemangleFuncName(GetCustomWord(addr, self.__processorWordSize)) 285 | 286 | addr += self.__processorWordSize 287 | i += 1 288 | 289 | def __MakeClassName(self) -> None: 290 | classNameAddr = self.__GetClassNameAddr() 291 | classNameLen = Byte(classNameAddr) 292 | 293 | ida_bytes.del_items( 294 | classNameAddr, 295 | ida_bytes.DELIT_DELNAMES, 296 | classNameLen + 1 297 | ) 298 | 299 | MakeStr_PASCAL(classNameAddr) 300 | MakeName(classNameAddr, self.__classInfo["Name"] + "_ClassName") 301 | MakeCustomWord(self.__VMTaddr, self.__processorWordSize) 302 | 303 | ida_name.set_name( 304 | self.__VMTaddr, 305 | self.__classInfo["FullName"], 306 | ida_name.SN_NOCHECK 307 | ) 308 | 309 | def __ResolveParent(self, parentClassAddr: int) -> None: 310 | if ida_bytes.is_loaded(parentClassAddr) and \ 311 | parentClassAddr != 0 and \ 312 | not ida_name.get_name(parentClassAddr).startswith("VMT_"): 313 | try: 314 | delphiClass = DelphiClass( 315 | parentClassAddr, 316 | self.__delphiVersion 317 | ) 318 | delphiClass.MakeClass() 319 | except DelphiHelperError as e: 320 | print(f"[ERROR] {e.msg}") 321 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_TypeInfo_tkClass.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's TypeInfo tkClass 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import ida_name 11 | from DelphiHelper.core.FieldEnum import FieldEnum 12 | from DelphiHelper.util.ida import * 13 | 14 | 15 | class TypeInfo_tkClass(object): 16 | 17 | def __init__( 18 | self, 19 | typeDataAddr: int, 20 | typeName: str, 21 | delphiVersion: int) -> None: 22 | self.__delphiVersion = delphiVersion 23 | self.__fieldEnum = None 24 | self.__processorWordSize = GetProcessorWordSize() 25 | self.__typeName = typeName 26 | self.__typeDataAddr = typeDataAddr 27 | self.__propDataAddr = (typeDataAddr 28 | + 2 * self.__processorWordSize 29 | + 3 30 | + Byte(typeDataAddr + 2 * self.__processorWordSize + 2)) 31 | 32 | def CreateTypeData(self) -> None: 33 | addr = self.__typeDataAddr 34 | 35 | MakeCustomWord(addr, self.__processorWordSize) 36 | ida_bytes.set_cmt(addr, "TypeData.ClassType", 0) 37 | 38 | addr += self.__processorWordSize 39 | MakeCustomWord(addr, self.__processorWordSize) 40 | ida_bytes.set_cmt(addr, "TypeData.ParentInfo", 0) 41 | 42 | typeInfoAddr = GetCustomWord(addr, self.__processorWordSize) 43 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 44 | typeInfo = TypeInfo(self.__delphiVersion) 45 | typeInfo.ResolveTypeInfo(typeInfoAddr, True) 46 | 47 | addr += self.__processorWordSize 48 | MakeWord(addr) 49 | ida_bytes.set_cmt(addr, "TypeData.PropCount", 0) 50 | 51 | addr += 2 52 | MakeStr_PASCAL(addr) 53 | ida_bytes.set_cmt(addr, "TypeData.UnitName", 0) 54 | 55 | MakeWord(self.__propDataAddr) 56 | ida_bytes.set_cmt(self.__propDataAddr, "TypeData.PropData.PropCount", 0) 57 | 58 | propCount = Word(self.__propDataAddr) 59 | addr = self.__propDataAddr + 2 60 | 61 | for i in range(propCount): 62 | addr = self.__CreatePropDataRecord(addr) 63 | 64 | propCount = Word(addr) 65 | 66 | if propCount != 0 and propCount <= 0xff: 67 | if (Byte(addr + 2) == 2) or (Byte(addr + 2) == 3): 68 | MakeWord(addr) 69 | addr += 2 70 | 71 | for i in range(propCount): 72 | MakeByte(addr) 73 | MakeCustomWord(addr + 1, self.__processorWordSize) 74 | 75 | nameAddr = GetCustomWord(addr + 1, self.__processorWordSize) 76 | name = ida_name.get_name(nameAddr) 77 | if self.__typeName not in name: 78 | propDataRecordAddr = GetCustomWord( 79 | addr + 1, 80 | self.__processorWordSize 81 | ) 82 | self.__CreatePropDataRecord(propDataRecordAddr) 83 | 84 | MakeWord(addr + 1 + self.__processorWordSize) 85 | addr += (1 + self.__processorWordSize 86 | + Word(addr + 1 + self.__processorWordSize)) 87 | 88 | def __CreatePropDataRecord(self, addr: int) -> None: 89 | nameAddr = addr + 4 * self.__processorWordSize + 10 90 | recordSize = 4 * self.__processorWordSize + 11 + Byte(nameAddr) 91 | nextRecordAddr = addr + recordSize 92 | 93 | typeInfoAddr = GetCustomWord(addr, self.__processorWordSize) 94 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 95 | typeInfo = TypeInfo(self.__delphiVersion) 96 | typeInfo.ResolveTypeInfo(typeInfoAddr, True) 97 | 98 | MakeCustomWord(addr, self.__processorWordSize) 99 | ida_bytes.set_cmt(addr, "TypeData.PropData.PropType", 0) 100 | 101 | addr += self.__processorWordSize 102 | MakeCustomWord(addr, self.__processorWordSize) 103 | ida_bytes.set_cmt(addr, "TypeData.PropData.GetProc", 0) 104 | 105 | shiftCount = (self.__processorWordSize - 1) * 8 106 | bitmask = GetCustomWord(addr, self.__processorWordSize) >> shiftCount 107 | if bitmask & 0xC0 == 0: 108 | MakeName( 109 | GetCustomWord(addr, self.__processorWordSize), 110 | self.__typeName + "_Get" + GetStr_PASCAL(nameAddr) 111 | ) 112 | 113 | addr += self.__processorWordSize 114 | MakeCustomWord(addr, self.__processorWordSize) 115 | ida_bytes.set_cmt(addr, "TypeData.PropData.SetProc", 0) 116 | 117 | bitmask = GetCustomWord(addr, self.__processorWordSize) >> shiftCount 118 | if bitmask & 0xC0 == 0: 119 | MakeName( 120 | GetCustomWord(addr, self.__processorWordSize), 121 | self.__typeName + "_Set" + GetStr_PASCAL(nameAddr) 122 | ) 123 | 124 | addr += self.__processorWordSize 125 | MakeCustomWord(addr, self.__processorWordSize) 126 | ida_bytes.set_cmt(addr, "TypeData.PropData.StoredProc", 0) 127 | 128 | addr += self.__processorWordSize 129 | MakeDword(addr) 130 | ida_bytes.set_cmt(addr, "TypeData.PropData.Index", 0) 131 | 132 | addr += 4 133 | MakeDword(addr) 134 | ida_bytes.set_cmt(addr, "TypeData.PropData.Default", 0) 135 | 136 | addr += 4 137 | MakeWord(addr) 138 | ida_bytes.set_cmt(addr, "TypeData.PropData.NameIndex", 0) 139 | 140 | MakeStr_PASCAL(nameAddr) 141 | ida_bytes.set_cmt(nameAddr, "TypeData.PropData.Name", 0) 142 | 143 | MakeName( 144 | nextRecordAddr - recordSize, 145 | self.__typeName + "_" + GetStr_PASCAL(nameAddr) 146 | ) 147 | 148 | return nextRecordAddr 149 | 150 | def DeleteTypeData(self) -> None: 151 | size = (2 * self.__processorWordSize 152 | + 5 153 | + Byte(self.__typeDataAddr + 2 * self.__processorWordSize + 2)) 154 | 155 | ida_bytes.del_items( 156 | self.__typeDataAddr, 157 | ida_bytes.DELIT_DELNAMES, 158 | size 159 | ) 160 | 161 | propCount = Word(self.__propDataAddr) 162 | addr = self.__propDataAddr + 2 163 | 164 | for i in range(propCount): 165 | addr = self.__DeletePropDataRecord(addr) 166 | 167 | propCount = Word(addr) 168 | 169 | if propCount != 0: 170 | if Byte(addr + 2) == 2 or Byte(addr + 2) == 3: 171 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, 2) 172 | addr += 2 173 | 174 | for i in range(propCount): 175 | ida_bytes.del_items( 176 | addr, 177 | ida_bytes.DELIT_DELNAMES, 178 | 3 + self.__processorWordSize 179 | ) 180 | 181 | propDataRecordAddr = GetCustomWord( 182 | addr + 1, 183 | self.__processorWordSize 184 | ) 185 | self.__DeletePropDataRecord(propDataRecordAddr) 186 | 187 | addr += (self.__processorWordSize 188 | + 1 189 | + Word(addr + self.__processorWordSize + 1)) 190 | 191 | def __DeletePropDataRecord(self, addr: int) -> int: 192 | recordSize = (4 * self.__processorWordSize 193 | + 11 194 | + Byte(addr + 4 * self.__processorWordSize + 10)) 195 | ida_bytes.del_items(addr, ida_bytes.DELIT_DELNAMES, recordSize) 196 | return addr + recordSize 197 | 198 | def ExtractData_TypeData(self, fieldEnum: FieldEnum) -> None: 199 | self.__fieldEnum = fieldEnum 200 | if self.__fieldEnum is None: 201 | return 202 | 203 | propCount = Word(self.__propDataAddr) 204 | addr = self.__propDataAddr + 2 205 | 206 | for i in range(propCount): 207 | addr = self.__ExtractData_PropDataRecord(addr) 208 | 209 | propCount = Word(addr) 210 | 211 | if propCount != 0 and \ 212 | propCount <= 0xff and \ 213 | (Byte(addr + 2) == 2 or Byte(addr + 2) == 3): 214 | addr += 2 215 | 216 | for i in range(propCount): 217 | propDataRecordAddr = GetCustomWord( 218 | addr + 1, 219 | self.__processorWordSize 220 | ) 221 | self.__ExtractData_PropDataRecord(propDataRecordAddr) 222 | 223 | addr += (self.__processorWordSize 224 | + 1 225 | + Word(addr + self.__processorWordSize + 1)) 226 | 227 | def __ExtractData_PropDataRecord(self, addr: int) -> int: 228 | nameAddr = addr + 4 * self.__processorWordSize + 10 229 | getProcEntry = GetCustomWord( 230 | addr + self.__processorWordSize, 231 | self.__processorWordSize 232 | ) 233 | setProcEntry = GetCustomWord( 234 | addr + 2 * self.__processorWordSize, 235 | self.__processorWordSize 236 | ) 237 | recordSize = 4 * self.__processorWordSize + 11 + Byte(nameAddr) 238 | shiftVal = (self.__processorWordSize - 1) * 8 239 | 240 | if self.__processorWordSize == 4: 241 | mask1 = 0x00FFFFFF 242 | else: 243 | mask1 = 0x00FFFFFFFFFFFFFF 244 | 245 | typeInfoAddr = GetCustomWord(addr, self.__processorWordSize) 246 | 247 | if ida_bytes.is_loaded(typeInfoAddr) and typeInfoAddr != 0: 248 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 249 | typeInfo = TypeInfo( 250 | self.__delphiVersion, 251 | typeInfoAddr + self.__processorWordSize 252 | ) 253 | typeName = typeInfo.GetTypeName() 254 | 255 | if ((getProcEntry >> shiftVal) & 0xF0 != 0) and \ 256 | ((setProcEntry >> shiftVal) & 0xF0 != 0): 257 | if getProcEntry == setProcEntry: 258 | self.__fieldEnum.AddMember( 259 | typeName, 260 | GetStr_PASCAL(nameAddr), 261 | setProcEntry & mask1 262 | ) 263 | else: 264 | self.__fieldEnum.AddMember( 265 | typeName, 266 | GetStr_PASCAL(nameAddr) + "_Get", 267 | getProcEntry & mask1 268 | ) 269 | self.__fieldEnum.AddMember( 270 | typeName, 271 | GetStr_PASCAL(nameAddr) + "_Set", 272 | setProcEntry & mask1 273 | ) 274 | else: 275 | if (getProcEntry >> shiftVal) & 0xF0 != 0: 276 | self.__fieldEnum.AddMember( 277 | typeName, 278 | GetStr_PASCAL(nameAddr), 279 | getProcEntry & mask1 280 | ) 281 | if (setProcEntry >> shiftVal) & 0xF0 != 0: 282 | self.__fieldEnum.AddMember( 283 | typeName, 284 | GetStr_PASCAL(nameAddr), 285 | setProcEntry & mask1 286 | ) 287 | 288 | return addr + recordSize 289 | -------------------------------------------------------------------------------- /DelphiHelper/core/DFMParser.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to search for, parse and extract data from Delphi's DFM 3 | # resource 4 | # 5 | # Copyright (c) 2020-2025 ESET 6 | # Author: Juraj Horňák 7 | # See LICENSE file for redistribution. 8 | 9 | 10 | import ida_bytes 11 | import ida_kernwin 12 | import ida_name 13 | from DelphiHelper.core.DFMFinder import * 14 | from DelphiHelper.core.DelphiClass import * 15 | from DelphiHelper.core.DelphiForm import * 16 | from DelphiHelper.util.exception import DelphiHelperError 17 | 18 | 19 | def ParseDFMs( 20 | delphiVersion: int 21 | ) -> list[tuple[DelphiObject, list[tuple[str, int]], int]]: 22 | dfmList = DFMFinder().GetDFMList() 23 | numOfDfms = len(dfmList) 24 | delphiFormList = list() 25 | 26 | ida_kernwin.show_wait_box("NODELAY\nHIDECANCEL\nProcessing Delphi file's DFMs...") 27 | 28 | try: 29 | for i, dfmEntry in enumerate(dfmList): 30 | msg = "HIDECANCEL\nProcessing Delphi file's DFMs (%d/%d)" \ 31 | % (i + 1, numOfDfms) 32 | ida_kernwin.replace_wait_box(msg) 33 | 34 | data = ida_bytes.get_bytes(dfmEntry[0], dfmEntry[1]) 35 | 36 | if data and (ida_bytes.is_loaded(dfmEntry[0] + dfmEntry[1] - 1) or dfmEntry[1] == 10000000): 37 | dfmEntryParser = DFMParser(data, dfmEntry[1]) 38 | 39 | if dfmEntryParser.CheckSignature(): 40 | methodList = list() 41 | VMTAddr = 0 42 | delphiDFM = dfmEntryParser.ParseForm() 43 | 44 | try: 45 | delphiRTTI = DelphiClass( 46 | 0, 47 | delphiVersion, 48 | delphiDFM.GetClassName() 49 | ) 50 | 51 | VMTAddr = delphiRTTI.GetVMTAddress() 52 | if not ida_name.get_name(VMTAddr).startswith("VMT_"): 53 | delphiRTTI.MakeClass() 54 | 55 | methodList = delphiRTTI.GetMethods() 56 | except DelphiHelperError: 57 | print(f"[WARNING] RTTI Class {delphiDFM.GetClassName()} not found!") 58 | 59 | delphiFormList.append((delphiDFM, methodList, VMTAddr)) 60 | 61 | del dfmEntryParser 62 | 63 | return delphiFormList 64 | finally: 65 | ida_kernwin.hide_wait_box() 66 | 67 | 68 | class DFMParser(object): 69 | 70 | def __init__(self, data: bytes, dataSize: int) -> None: 71 | self.__resData = data 72 | self.__resDataSize = dataSize 73 | self.__resDataPos = 0 74 | 75 | def ParseForm(self) -> DelphiObject | None: 76 | if self.__ReadSignature(): 77 | delphiForm = self.__ParseObject() 78 | return delphiForm 79 | return None 80 | 81 | def CheckSignature(self) -> bool: 82 | if self.__resDataSize < 4: 83 | return False 84 | return self.__resData.startswith(b"TPF0") 85 | 86 | def __ParseProperty( 87 | self, 88 | parentDelphiObj: DelphiObject) -> DelphiProperty | None: 89 | try: 90 | if self.__ReadRawData(1, 0)[0] == 0: 91 | self.__ReadByte() 92 | return None 93 | except DelphiHelperError: 94 | return None 95 | 96 | propName = self.__ReadString().decode() 97 | propType = self.__ReadByte() 98 | 99 | if propName is None or propType is None: 100 | return None 101 | 102 | propValue = self.__ReadPropertyValue(propType) 103 | 104 | delphiProp = DelphiProperty( 105 | propName, 106 | propType, 107 | propValue, 108 | parentDelphiObj 109 | ) 110 | parentDelphiObj.AddProperty(delphiProp) 111 | 112 | return delphiProp 113 | 114 | def __ParseObject( 115 | self, 116 | parentDelphiObj: DelphiObject | None = None 117 | ) -> DelphiObject | None: 118 | try: 119 | if self.__ReadRawData(1, 0)[0] == 0: 120 | self.__ReadByte() 121 | return None 122 | except DelphiHelperError: 123 | return None 124 | 125 | if not self.__ReadPrefix(): 126 | className = self.__ReadString().decode() 127 | objectName = self.__ReadString().decode() 128 | 129 | if className is not None and objectName is not None: 130 | childDelphiObj = DelphiObject( 131 | className, 132 | objectName, 133 | parentDelphiObj 134 | ) 135 | 136 | while self.__ParseProperty(childDelphiObj) is not None: 137 | pass 138 | while self.__ParseObject(childDelphiObj) is not None: 139 | pass 140 | 141 | if parentDelphiObj is not None: 142 | parentDelphiObj.AddChildObject(childDelphiObj) 143 | 144 | return childDelphiObj 145 | else: 146 | raise DelphiHelperError("ClassName or ObjName is None") 147 | 148 | def __ReadSignature(self) -> bool: 149 | return self.__ReadRawData(4) == b"TPF0" 150 | 151 | def __OutOfDataCheck(self, size: int) -> bool: 152 | if self.__resDataPos + size - 1 >= self.__resDataSize: 153 | raise DelphiHelperError("DMF corrupted: No more data to read!") 154 | return False 155 | 156 | def __ReadRawData(self, size: int, shift: int = 1) -> bytes: 157 | if size < 0: 158 | size = 0 159 | 160 | self.__OutOfDataCheck(size) 161 | rawData = self.__resData[self.__resDataPos:self.__resDataPos+size] 162 | 163 | if shift == 1: 164 | self.__resDataPos += size 165 | 166 | return rawData 167 | 168 | def __ReadByte(self) -> int: 169 | return self.__ReadRawData(1)[0] 170 | 171 | def __ReadWord(self) -> int: 172 | rawData = self.__ReadRawData(2) 173 | value = 0 174 | 175 | for i in range(2): 176 | value += (rawData[i] << i*8) 177 | return value 178 | 179 | def __ReadDword(self) -> int: 180 | rawData = self.__ReadRawData(4) 181 | value = 0 182 | 183 | for i in range(4): 184 | value += (rawData[i] << i*8) 185 | return value 186 | 187 | def __ReadString(self) -> bytes: 188 | return self.__ReadRawData(self.__ReadByte()) 189 | 190 | def __ReadLString(self) -> bytes: 191 | return self.__ReadRawData(self.__ReadDword()) 192 | 193 | def __ReadWString(self) -> bytes: 194 | return self.__ReadRawData(self.__ReadDword() * 2) 195 | 196 | def __ReadData(self) -> bytes: 197 | return self.__ReadRawData(self.__ReadDword()) 198 | 199 | def __ReadSingle(self) -> bytes: 200 | return self.__ReadRawData(4) 201 | 202 | def __ReadExtended(self) -> bytes: 203 | return self.__ReadRawData(10) 204 | 205 | def __ReadDouble(self) -> bytes: 206 | return self.__ReadRawData(8) 207 | 208 | def __ReadCurrency(self) -> bytes: 209 | return self.__ReadRawData(8) 210 | 211 | def __ReadDate(self) -> bytes: 212 | return self.__ReadRawData(8) 213 | 214 | def __ReadInt64(self) -> bytes: 215 | return self.__ReadRawData(8) 216 | 217 | def __ReadList(self) -> list: 218 | listElementType = self.__ReadByte() 219 | listElements = list() 220 | 221 | while listElementType != 0: 222 | listElements.append( 223 | (self.__ReadPropertyValue(listElementType), listElementType) 224 | ) 225 | listElementType = self.__ReadByte() 226 | return listElements 227 | 228 | def __ReadSet(self) -> list: 229 | setElements = list() 230 | 231 | while True: 232 | elem = self.__ReadString() 233 | 234 | if len(elem) != 0: 235 | setElements.append(elem) 236 | else: 237 | break 238 | 239 | return setElements 240 | 241 | def __ReadCollection(self) -> list: 242 | collectionElements = list() 243 | 244 | while True: 245 | elementValue = None 246 | elementType = self.__ReadByte() 247 | 248 | if elementType == 0: 249 | break 250 | elif elementType == 2 or elementType == 3 or elementType == 4: 251 | elementValue = self.__ReadPropertyValue(elementType) 252 | elif elementType == 1: 253 | attrList = list() 254 | flag = True 255 | 256 | while flag: 257 | attrName = self.__ReadString().decode() 258 | 259 | if len(attrName) == 0: 260 | flag = False 261 | else: 262 | attrType = self.__ReadByte() 263 | attrList.append(( 264 | attrName, 265 | self.__ReadPropertyValue(attrType), 266 | attrType 267 | )) 268 | else: 269 | raise DelphiHelperError("Invalid collection format!") 270 | 271 | collectionElements.append((elementValue, attrList)) 272 | 273 | return collectionElements 274 | 275 | def __ReadPrefix(self) -> bool: 276 | if self.__ReadRawData(1, 0)[0] & 0xf0 == 0xf0: 277 | prefix = self.__ReadByte() 278 | 279 | flags = prefix & 0x0f 280 | 281 | if flags > 7: 282 | raise DelphiHelperError("Unsupported DFM Prefix.") 283 | 284 | if (flags & 2) != 0: 285 | propType = self.__ReadByte() 286 | 287 | if propType == 2 or propType == 3 or propType == 4: 288 | self.__ReadPropertyValue(propType) 289 | else: 290 | raise DelphiHelperError("Unsupported DFM Prefix.") 291 | 292 | return False 293 | 294 | def __ReadPropertyValue( 295 | self, 296 | propType: int) -> bytes | int | list | str: 297 | match propType: 298 | # Null 299 | case 0: return "Null" 300 | # List 301 | case 1: return self.__ReadList() 302 | # Int8 303 | case 2: return self.__ReadByte() 304 | # Int16 305 | case 3: return self.__ReadWord() 306 | # Int32 307 | case 4: return self.__ReadDword() 308 | # Extended 309 | case 5: return self.__ReadExtended() 310 | # String 311 | case 6: return self.__ReadString() 312 | # Ident 313 | case 7: return self.__ReadString() 314 | # False 315 | case 8: return 0 316 | # True 317 | case 9: return 1 318 | # Binary 319 | case 10: return self.__ReadData() 320 | # Set 321 | case 11: return self.__ReadSet() 322 | # LString 323 | case 12: return self.__ReadLString() 324 | # Nil 325 | case 13: return "Nil" 326 | # Collection 327 | case 14: return self.__ReadCollection() 328 | # Single 329 | case 15: return self.__ReadSingle() 330 | # Currency 331 | case 16: return self.__ReadCurrency() 332 | # Date 333 | case 17: return self.__ReadDate() 334 | # WString 335 | case 18: return self.__ReadWString() 336 | # Int64 337 | case 19: return self.__ReadInt64() 338 | # UTF8String 339 | case 20: return self.__ReadLString() 340 | # Double 341 | case 21: return self.__ReadDouble() 342 | case _: 343 | raise DelphiHelperError("Unsupported property type: " + str(propType)) 344 | -------------------------------------------------------------------------------- /DelphiHelper/core/IDRKBLoader.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to load IDR KB signatures and implements GUI for 3 | # IDRKBLoader 4 | # 5 | # Copyright (c) 2020-2025 ESET 6 | # Author: Juraj Horňák 7 | # See LICENSE file for redistribution. 8 | 9 | 10 | import ida_funcs 11 | import ida_idaapi 12 | import ida_kernwin 13 | import ida_name 14 | import os 15 | from DelphiHelper.core.IDRKBParser import GetDelphiVersion, KBParser 16 | from DelphiHelper.util.delphi import GetUnits, ParseMangledFunctionName 17 | from DelphiHelper.util.exception import DelphiHelperError 18 | from DelphiHelper.util.ida import ( 19 | find_bytes, FixName, MakeFunction, MakeName, Is64bit 20 | ) 21 | from PyQt5 import QtGui, QtCore, QtWidgets 22 | 23 | 24 | def KBLoader(custom: bool = False) -> None: 25 | KBFile = chooseKBFile() 26 | if KBFile is not None: 27 | if custom: 28 | global _KBLoader 29 | try: 30 | _KBLoader 31 | except Exception: 32 | _KBLoader = IDRKBLoaderDialog(KBFile) 33 | _KBLoader.Show() 34 | else: 35 | kbLoader = IDRKBLoader(["SysInit", "System"], KBFile) 36 | kbLoader.LoadIDRKBSignatures(GetDelphiVersion()) 37 | 38 | 39 | def chooseKBFile() -> str or None: 40 | question = "Do you want to choose the IDR KB file manually? (If not, KB autodetection will be performed.)" 41 | result = ida_kernwin.ask_yn(ida_kernwin.ASKBTN_NO, question) 42 | 43 | if result == ida_kernwin.ASKBTN_YES: 44 | kbFilePath = ida_kernwin.ask_file(False, "*.*", "Select IDR KB file") 45 | elif result == ida_kernwin.ASKBTN_NO: 46 | kbFilePath = str() 47 | else: 48 | kbFilePath = None 49 | 50 | return kbFilePath 51 | 52 | 53 | class IDRKBLoaderDialog(ida_kernwin.PluginForm): 54 | 55 | def __init__(self, KBFile: str) -> None: 56 | super(IDRKBLoaderDialog, self).__init__() 57 | self.__KBFile = KBFile 58 | 59 | def OnCreate(self, form) -> None: 60 | msg = "NODELAY\nHIDECANCEL\nExtracting list of imported units..." 61 | ida_kernwin.show_wait_box(msg) 62 | unitList = sorted(GetUnits()) 63 | ida_kernwin.hide_wait_box() 64 | 65 | if len(unitList): 66 | checkList = ChecklistDialog( 67 | self.__clink__, 68 | self.FormToPyQtWidget(form), 69 | unitList, 70 | self.__KBFile 71 | ) 72 | 73 | def OnClose(self, form) -> None: 74 | global _KBLoader 75 | del _KBLoader 76 | 77 | def Show(self): 78 | return ida_kernwin.plgform_show( 79 | self.__clink__, 80 | self, 81 | "IDR Knowledge Base Loader", 82 | ida_kernwin.WOPN_PERSIST | 0x20 83 | ) 84 | 85 | 86 | class ChecklistDialog(QtWidgets.QDialog): 87 | 88 | def __init__( 89 | self, 90 | clink, 91 | parent, 92 | unitList: list[str], 93 | KBFile: str, 94 | checked: bool = True) -> None: 95 | super(ChecklistDialog, self).__init__(parent) 96 | 97 | self.__KBFile = KBFile 98 | self.clink = clink 99 | self.parent = parent 100 | 101 | self.model = QtGui.QStandardItemModel() 102 | self.listView = QtWidgets.QListView() 103 | 104 | for unit in unitList: 105 | item = QtGui.QStandardItem(unit) 106 | item.setCheckable(True) 107 | item.setEditable(False) 108 | 109 | if checked: 110 | item.setCheckState(QtCore.Qt.Checked) 111 | else: 112 | item.setCheckState(QtCore.Qt.Unchecked) 113 | 114 | self.model.appendRow(item) 115 | 116 | self.listView.setModel(self.model) 117 | 118 | self.loadButton = QtWidgets.QPushButton("Load") 119 | self.cancelButton = QtWidgets.QPushButton("Cancel") 120 | self.selectButton = QtWidgets.QPushButton("Select All") 121 | self.unselectButton = QtWidgets.QPushButton("Unselect All") 122 | 123 | hbox = QtWidgets.QHBoxLayout() 124 | hbox.addStretch(1) 125 | hbox.addWidget(self.loadButton) 126 | hbox.addWidget(self.cancelButton) 127 | hbox.addWidget(self.selectButton) 128 | hbox.addWidget(self.unselectButton) 129 | 130 | vbox = QtWidgets.QVBoxLayout(self) 131 | label = QtWidgets.QLabel("Select units whose IDR KB signatures will be loaded") 132 | label.setFont(QtGui.QFont("Arial", 8)) 133 | vbox.addWidget(label) 134 | label = QtWidgets.QLabel("Warning: Loading signatures for all units can last very long time") 135 | label.setFont(QtGui.QFont("Arial", 8)) 136 | vbox.addWidget(label) 137 | vbox.addWidget(self.listView) 138 | vbox.addLayout(hbox) 139 | 140 | self.parent.setLayout(vbox) 141 | 142 | self.loadButton.clicked.connect(self.onLoad) 143 | self.cancelButton.clicked.connect(self.onCanceled) 144 | self.selectButton.clicked.connect(self.select) 145 | self.unselectButton.clicked.connect(self.unselect) 146 | 147 | def onLoad(self) -> None: 148 | choices = list() 149 | 150 | for i in range(self.model.rowCount()): 151 | if self.model.item(i).checkState() == QtCore.Qt.Checked: 152 | choices.append(self.model.item(i).text()) 153 | 154 | ida_kernwin.plgform_close(self.clink, 0x2) 155 | 156 | try: 157 | if choices: 158 | IDRKBLoader(choices, self.__KBFile).LoadIDRKBSignatures() 159 | except DelphiHelperError as e: 160 | e.print() 161 | 162 | def onCanceled(self) -> None: 163 | ida_kernwin.plgform_close(self.clink, 0x2) 164 | 165 | def select(self) -> None: 166 | for i in range(self.model.rowCount()): 167 | item = self.model.item(i) 168 | item.setCheckState(QtCore.Qt.Checked) 169 | 170 | def unselect(self) -> None: 171 | for i in range(self.model.rowCount()): 172 | item = self.model.item(i) 173 | item.setCheckState(QtCore.Qt.Unchecked) 174 | 175 | 176 | class IDRKBLoader(object): 177 | 178 | def __init__( 179 | self, 180 | units: list[str], 181 | KBFile: str) -> None: 182 | self.__unitList = units 183 | self.__KBFile = KBFile 184 | 185 | def LoadIDRKBSignatures(self, delphiVersion: int = 0) -> None: 186 | if self.__KBFile == str(): 187 | if delphiVersion == 0: 188 | ida_kernwin.show_wait_box("NODELAY\nHIDECANCEL\nTrying to determine Delphi version...") 189 | delphiVersion = GetDelphiVersion() 190 | ida_kernwin.hide_wait_box() 191 | 192 | if delphiVersion == -1: 193 | delphiVersion = 2014 194 | 195 | self.__KBFile = self.__getKBFilePath(delphiVersion) 196 | 197 | self.__loadSignatures(self.__KBFile, self.__unitList) 198 | 199 | def __isDifferentFunctionName( 200 | self, 201 | origName: str, 202 | signatureName: str) -> bool: 203 | if origName.startswith("KB_"): 204 | if origName == signatureName or \ 205 | origName == signatureName + "_0": 206 | return False 207 | else: 208 | return True 209 | 210 | if "@" in origName: 211 | origName = ParseMangledFunctionName(origName) 212 | 213 | if origName == signatureName[3:]: 214 | return False 215 | 216 | return True 217 | 218 | def __renameFunction(self, unit: str, function: tuple[str, str]) -> bool: 219 | funcAddr = find_bytes(function[1]) 220 | if funcAddr != ida_idaapi.BADADDR: 221 | if function[0][0] == "@": 222 | functionName = FixName("KB_" + unit + "_" + function[0][1:]) 223 | else: 224 | functionName = FixName("KB_" + unit + "_" + function[0]) 225 | 226 | funcAddr2 = find_bytes(function[1], funcAddr + 1) 227 | if funcAddr2 == ida_idaapi.BADADDR: 228 | funcStruct = ida_funcs.get_func(funcAddr) 229 | if funcStruct: 230 | funcAddr = funcStruct.start_ea 231 | else: 232 | MakeFunction(funcAddr) 233 | print(f"[INFO] Making a new function on 0x{funcAddr:X}") 234 | origFunctionName = ida_name.get_name(funcAddr) 235 | 236 | if not self.__isDifferentFunctionName(origFunctionName, functionName): 237 | return False 238 | 239 | functionCmt = ida_funcs.get_func_cmt(funcAddr, 1) 240 | if not self.__isDifferentFunctionName(origFunctionName, functionName) or \ 241 | (functionCmt and functionName in functionCmt): 242 | return False 243 | 244 | if origFunctionName.startswith("sub_") or \ 245 | origFunctionName.startswith("unknown_"): 246 | print( 247 | f"[INFO] Renaming: {origFunctionName} -> {functionName} (0x{funcAddr:X})" 248 | ) 249 | MakeName(funcAddr, functionName) 250 | else: 251 | cmt = ida_funcs.get_func_cmt(funcAddr, 1) 252 | if cmt: 253 | cmt = functionName + "\n" + cmt 254 | else: 255 | cmt = functionName 256 | 257 | print( 258 | f"[INFO] Adding alternative name: {origFunctionName} -> {functionName} (0x{funcAddr:X})" 259 | ) 260 | ida_funcs.set_func_cmt(funcAddr, cmt, 1) 261 | return True 262 | return False 263 | 264 | def __getKBFilePath(self, delphiVersion: int) -> str: 265 | if Is64bit(): 266 | KBPath = os.path.join( 267 | os.path.dirname(__file__), 268 | "..", 269 | "IDR_KB", 270 | "IDR64", 271 | f"syskb{delphiVersion}.bin" 272 | ) 273 | else: 274 | KBPath = os.path.join( 275 | os.path.dirname(__file__), 276 | "..", 277 | "IDR_KB", 278 | "IDR", 279 | f"kb{delphiVersion}.bin" 280 | ) 281 | KBPath = os.path.normpath(KBPath) 282 | 283 | if not os.path.exists(KBPath): 284 | print(f"[WARNING] KB file \"{KBPath}\" not found!") 285 | 286 | msg = f"KB file \"{KBPath}\" not found!\nNote: read the README.md for KB file location." 287 | ida_kernwin.warning(msg) 288 | 289 | raise DelphiHelperError() 290 | 291 | return KBPath 292 | 293 | def __loadSignatures( 294 | self, 295 | KBFile: str, 296 | units: list[str]) -> None: 297 | ida_kernwin.show_wait_box("NODELAY\nLoading IDR Knowledge base signatures...") 298 | 299 | try: 300 | kb = KBParser(KBFile) 301 | counter = 0 302 | unitCounter = 0 303 | 304 | for unit in units: 305 | unitCounter += 1 306 | moduleID = kb.GetModuleID(unit) 307 | 308 | if moduleID != -1: 309 | print(f"[INFO] Loading function signatures for \"{unit}\" unit") 310 | 311 | firstIndex, lastIndex = kb.GetProcIdxs(moduleID) 312 | functions = kb.GetFunctions(firstIndex, lastIndex) 313 | i = 0 314 | 315 | for func in functions: 316 | i += 1 317 | 318 | msg = (f"Loading IDR Knowledge base signatures... Unit {unit} ({unitCounter}/{len(units)}) - Function ({i}/{len(functions)})") 319 | ida_kernwin.replace_wait_box(msg) 320 | 321 | if self.__renameFunction(unit, func): 322 | counter += 1 323 | 324 | if ida_kernwin.user_cancelled(): 325 | print(f"[INFO] Applied signatures: {str(counter)}") 326 | return 327 | else: 328 | print(f"[INFO] KB file does not include signatures for \"{unit}\" module") 329 | 330 | print(f"[INFO] Applied signatures: {str(counter)}") 331 | except DelphiHelperError as e: 332 | e.print() 333 | finally: 334 | ida_kernwin.hide_wait_box() 335 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiForm.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module implements DelphiObject and DelphiProperty classes storing data 3 | # extracted from Delphi's DFM 4 | # 5 | # Copyright (c) 2020-2025 ESET 6 | # Author: Juraj Horňák 7 | # See LICENSE file for redistribution. 8 | 9 | 10 | from __future__ import annotations 11 | import hashlib 12 | import ida_loader 13 | import idautils 14 | import os 15 | import struct 16 | 17 | 18 | PropertyTypes = ["Null", "List", "Int8", "Int16", "Int32", "Extended", 19 | "String", "Ident", "False", "True", "Binary", "Set", 20 | "LString", "Nil", "Collection", "Single", "Currency", "Date", 21 | "WString", "Int64", "UTF8String", "Double"] 22 | 23 | 24 | class DelphiProperty(object): 25 | 26 | def __init__( 27 | self, 28 | name: str, 29 | propType: int, 30 | value: bytes | int | list | str, 31 | parentObj: DelphiObject) -> None: 32 | self.__type = propType 33 | self.__name = name 34 | self.__value = value 35 | self.__parentObj = parentObj 36 | 37 | def GetType(self) -> int: 38 | return self.__type 39 | 40 | def GetTypeAsString(self) -> str: 41 | return PropertyTypes[self.__type] 42 | 43 | def GetName(self) -> str: 44 | return self.__name 45 | 46 | def GetParentObjName(self) -> str: 47 | return self.__parentObj.GetObjectName() 48 | 49 | def GetParentClassName(self) -> str: 50 | return self.__parentObj.GetClassName() 51 | 52 | def GetValue(self) -> bytes | int | list | str: 53 | return self.__value 54 | 55 | def GetValueAsString(self, flag: int = 0) -> None: 56 | return self.__PrintValue(self.__value, self.__type, flag) 57 | 58 | def PrintPropertyInfo(self, indentation: str) -> None: 59 | """Print Property Info""" 60 | 61 | # Binary 62 | if self.__type == 10: 63 | print(f"{indentation}{self.__name} = Binary data ... ({PropertyTypes[self.__type]})") 64 | else: 65 | print(f"{indentation}{self.__name} = {self.__PrintValue(self.__value, self.__type)} ({PropertyTypes[self.__type]})") 66 | 67 | def __PrintValue( 68 | self, 69 | data: bytes | int | list | str, 70 | dataType: int, 71 | flag: int = 0) -> str: 72 | match dataType: 73 | case 0: 74 | # "Null" 75 | return "Null" 76 | case 1: 77 | # "List" 78 | return self.__PrintList(data) 79 | case 2: 80 | # "Int8" 81 | return str(data) 82 | case 3: 83 | # "Int16" 84 | return str(data) 85 | case 4: 86 | # "Int32" 87 | return str(data) 88 | case 5: 89 | # "Extended" 90 | return self.__PrintExtended(data) 91 | case 6: 92 | # "String" 93 | return self.__PrintString(data) 94 | case 7: 95 | # "Ident" 96 | return data.decode() 97 | case 8: 98 | # "False" 99 | return "False" 100 | case 9: 101 | # "True" 102 | return "True" 103 | case 10: 104 | # "Binary" 105 | return self.__PrintBinary(data, flag) 106 | case 11: 107 | # "Set" 108 | return self.__PrintSet(data) 109 | case 12: 110 | # "LString" 111 | return self.__PrintString(data) 112 | case 13: 113 | # "Nil" 114 | return "Nil" 115 | case 14: 116 | # "Collection" 117 | return self.__PrintCollection(data) 118 | case 15: 119 | # "Single" 120 | return str(struct.unpack("f", data)[0]) 121 | case 16: 122 | # "Currency" 123 | return str(struct.unpack("d", data)[0]) 124 | case 17: 125 | # "Date" 126 | return str(struct.unpack("d", data)[0]) 127 | case 18: 128 | # "WString" 129 | return self.__PrintWString(data) 130 | case 19: 131 | # "Int64" 132 | return str(struct.unpack("q", data)[0]) 133 | case 20: 134 | # "UTF8String" 135 | return self.__PrintString(data) 136 | case 21: 137 | # "Double" 138 | return str(struct.unpack("d", data)[0]) 139 | case _: 140 | return "" 141 | 142 | def __PrintExtended(self, data: bytes) -> str: 143 | strValue = str() 144 | 145 | for a in data: 146 | strValue += "%02X" % (a) 147 | 148 | return strValue 149 | 150 | def __PrintBinary(self, data: bytes, flag: int = 0) -> str: 151 | if flag == 1: 152 | hashSha1 = hashlib.sha1() 153 | hashSha1.update(data) 154 | return hashSha1.hexdigest().upper() 155 | else: 156 | return self.__SaveDataToFile(data) 157 | 158 | def __PrintList(self, listData: list) -> str: 159 | if len(listData) == 0: 160 | strValue = "[]" 161 | else: 162 | strValue = "[" 163 | 164 | for item in listData: 165 | strValue += self.__PrintValue(item[0], item[1]) + ", " 166 | 167 | strValue = strValue[:-2] + "]" 168 | 169 | return strValue 170 | 171 | def __PrintCollection(self, data: list) -> str: 172 | if len(data) == 0: 173 | return "<>" 174 | else: 175 | strValue = "<" 176 | 177 | for collectionElem in data: 178 | if collectionElem[0] is not None: 179 | strValue += " [" + str(collectionElem[0]) + "]: " 180 | 181 | if len(collectionElem[1]) != 0: 182 | for attrElem in collectionElem[1]: 183 | strValue += (attrElem[0] 184 | + "=" 185 | + self.__PrintValue(attrElem[1], attrElem[2]) 186 | + " | ") 187 | 188 | strValue = strValue[:-3] 189 | 190 | strValue += ", " 191 | 192 | return strValue[:-2] + ">" 193 | 194 | def __PrintSet(self, data: list) -> str: 195 | if len(data) == 0: 196 | return "()" 197 | else: 198 | strValue = str() 199 | 200 | for elem in data: 201 | strValue += elem.decode() + ", " 202 | 203 | strValue = "(" + strValue[: -2] + ")" 204 | return strValue 205 | 206 | def __PrintString(self, rawdata: bytes) -> str: 207 | if len(rawdata) == 0: 208 | strValue = "\'\'" 209 | else: 210 | strValue = str(rawdata)[1:] 211 | 212 | return strValue 213 | 214 | def __PrintWString(self, rawData: bytes) -> str: 215 | data = list() 216 | 217 | for i in range(len(rawData) // 2): 218 | value = rawData[i*2] + (rawData[i*2 + 1] << 8) 219 | data.append(value) 220 | 221 | if len(data) == 0: 222 | strValue = "\'\'" 223 | else: 224 | strValue = "\'" 225 | 226 | for a in data: 227 | if a > 31 and a < 127: 228 | strValue += str(chr(a)) 229 | else: 230 | strValue += "\\u%04x" % (a) 231 | 232 | strValue += "\'" 233 | 234 | return strValue 235 | 236 | def __SaveDataToFile(self, data: bytes) -> str: 237 | """ Save extracted data to folder, the folder name is derived from the 238 | file name of the binary. i.e: file.exe -> _extracted_file_exe 239 | 240 | Args: 241 | data (bytes): data to save to the folder. 242 | 243 | Returns: 244 | path (str): return path of the folder. 245 | 246 | """ 247 | binaryFileName = ida_loader.get_path(ida_loader.PATH_TYPE_IDB) 248 | if binaryFileName.endswith(".idb") or binaryFileName.endswith(".i64"): 249 | binaryFileName = binaryFileName[:-4] 250 | binaryFileName = binaryFileName[binaryFileName.rfind(os.path.sep) + 1:] 251 | 252 | dataDir = os.path.abspath(idautils.GetIdbDir()) 253 | if not os.path.isdir(dataDir): 254 | print(f"[ERROR] No such directory: \"{dataDir}\". Trying to create it...") 255 | 256 | try: 257 | os.mkdir(dataDir) 258 | except FileNotFoundError: 259 | print(f"Failed to create directory: \"{dataDir}\". Extracted file not saved!") 260 | return "" 261 | 262 | dataDir = os.path.join( 263 | dataDir, 264 | "_extracted_" + binaryFileName.replace(".", "_") 265 | ) 266 | 267 | if not os.path.isdir(dataDir): 268 | os.mkdir(dataDir) 269 | 270 | data, ext = self.__PreprocessData(data) 271 | fileName = self.__GetFileName() + ext 272 | 273 | filePath = os.path.join(dataDir, fileName) 274 | with open(filePath, "wb") as f: 275 | print(f"[INFO] Saving file \"{filePath}\"") 276 | f.write(data) 277 | 278 | retPath = ("\".\\_extracted_" 279 | + binaryFileName.replace(".", "_") 280 | + "\\" 281 | + fileName 282 | + "\"") 283 | 284 | return retPath 285 | 286 | def __PreprocessData(self, data: bytes) -> tuple[bytes, str]: 287 | ext = ".bin" 288 | signature = data[:32] 289 | 290 | if b"TBitmap" in signature: 291 | data = data[12:] 292 | ext = ".bmp" 293 | elif b"TJPEGImage" in signature: 294 | data = data[15:] 295 | ext = ".jpeg" 296 | elif b"TWICImage" in signature: 297 | data = data[10:] 298 | ext = ".tif" 299 | elif b"TPngImage" in signature: 300 | data = data[10:] 301 | ext = ".png" 302 | elif b"TPNGGraphic" in signature: 303 | data = data[16:] 304 | ext = ".png" 305 | elif b"TPNGObject" in signature: 306 | data = data[11:] 307 | ext = ".png" 308 | elif signature[1:4] == b"PNG": 309 | ext = ".png" 310 | elif b"TGIFImage" in signature: 311 | data = data[10:] 312 | ext = ".gif" 313 | elif signature[28:31] == b"BM6": 314 | data = data[28:] 315 | ext = ".bmp" 316 | elif signature[4:6] == b"BM": 317 | flag = True 318 | 319 | for i in range(4): 320 | if data[i] != data[i + 6]: 321 | flag = False 322 | 323 | if flag: 324 | data = data[4:] 325 | ext = ".bmp" 326 | 327 | return data, ext 328 | 329 | def __GetFileName(self) -> str: 330 | parentObj = self.__parentObj 331 | fileName = str() 332 | 333 | while parentObj is not None: 334 | fileName = parentObj.GetObjectName()[:15] + "_" + fileName 335 | parentObj = parentObj.GetParentObject() 336 | 337 | return fileName[:len(fileName)-1] 338 | 339 | 340 | class DelphiObject(object): 341 | 342 | def __init__( 343 | self, 344 | className: str, 345 | objName: str, 346 | parentObj: DelphiObject | None) -> None: 347 | self.__className = className 348 | self.__objectName = objName 349 | self.__listOfChildrenObjects = list() 350 | self.__listOfProperties = list() 351 | self.__parentObj = parentObj 352 | 353 | def GetParentObject(self) -> DelphiObject | None: 354 | return self.__parentObj 355 | 356 | def GetObjectName(self) -> str: 357 | return self.__objectName 358 | 359 | def SetObjectName(self, objName: str) -> None: 360 | self.__objectName = objName 361 | 362 | def GetClassName(self) -> str: 363 | return self.__className 364 | 365 | def SetClassName(self, className: str) -> None: 366 | self.__className = className 367 | 368 | def GetPropertyList(self) -> list[DelphiProperty]: 369 | return self.__listOfProperties 370 | 371 | def AddProperty(self, prop: DelphiProperty) -> None: 372 | self.__listOfProperties.append(prop) 373 | 374 | def GetChildObjectList(self) -> list[DelphiObject]: 375 | return self.__listOfChildrenObjects 376 | 377 | def AddChildObject(self, childObj: DelphiObject) -> None: 378 | self.__listOfChildrenObjects.append(childObj) 379 | 380 | def PrintObjectInfo(self, indentation: str = "") -> None: 381 | print(f"{indentation}object {self.__objectName}: {self.__className}") 382 | 383 | for prop in self.__listOfProperties: 384 | prop.PrintPropertyInfo(indentation + " ") 385 | 386 | for obj in self.__listOfChildrenObjects: 387 | obj.PrintObjectInfo(indentation + " ") 388 | 389 | print(f"{indentation} end") 390 | -------------------------------------------------------------------------------- /DelphiHelper/core/DelphiClass_TypeInfo_tkRecord.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse and extract data from Delphi's TypeInfo tkRecord 3 | # 4 | # Copyright (c) 2020-2025 ESET 5 | # Author: Juraj Horňák 6 | # See LICENSE file for redistribution. 7 | 8 | 9 | import ida_bytes 10 | import ida_funcs 11 | import idc 12 | from DelphiHelper.util.delphi import GetParamRegister 13 | from DelphiHelper.util.ida import * 14 | 15 | 16 | class TypeInfo_tkRecord(object): 17 | 18 | def __init__( 19 | self, 20 | typeDataAddr: int, 21 | typeName: str, 22 | delphiVersion: int) -> None: 23 | self.__processorWordSize = GetProcessorWordSize() 24 | self.__typeDataAddr = typeDataAddr 25 | 26 | if len(typeName) == 0: 27 | self.__typeName = "Record_" + hex(self.__typeDataAddr) 28 | else: 29 | self.__typeName = typeName 30 | self.__delphiVersion = delphiVersion 31 | 32 | def CreateTypeData(self) -> None: 33 | addr = self.__typeDataAddr 34 | 35 | MakeDword(addr) 36 | ida_bytes.set_cmt(addr, "TypeData.Size", 0) 37 | addr += 4 38 | 39 | addr = self.__CreateManagedFields(addr) 40 | 41 | if self.__delphiVersion != -1 and self.__delphiVersion >= 2010: 42 | MakeByte(addr) 43 | ida_bytes.set_cmt(addr, "TypeData.NumOps", 0) 44 | numOps = Byte(addr) 45 | addr += 1 46 | 47 | if numOps: 48 | ida_bytes.set_cmt(addr, "TypeData.RecOps", 0) 49 | print("WARNING WARNING TypeData.RecOps") 50 | 51 | for i in range(numOps): 52 | MakeCustomWord(addr, self.__processorWordSize) 53 | addr += self.__processorWordSize 54 | 55 | addr = self.__CreateRecordTypeField(addr) 56 | 57 | MakeWord(addr) 58 | ida_bytes.set_cmt(addr, "TypeData.RecAttrData", 0) 59 | addr += Word(addr) 60 | 61 | if self.__delphiVersion >= 2012: 62 | addr = self.__CreateRecordTypeMethod(addr) 63 | 64 | def __CreateRecordTypeMethod(self, addr: int) -> int: 65 | MakeWord(addr) 66 | ida_bytes.set_cmt(addr, "TypeData.RecMethCnt", 0) 67 | recMethCnt = Word(addr) 68 | addr += 2 69 | 70 | if recMethCnt and recMethCnt <= 4096: 71 | ida_bytes.set_cmt(addr, "TypeData.RecMeths", 0) 72 | 73 | for i in range(recMethCnt): 74 | # Flags 75 | ida_bytes.set_cmt(addr, "TypeData.RecMeths.Flags", 0) 76 | MakeByte(addr) 77 | 78 | # Code 79 | MakeCustomWord(addr + 1, self.__processorWordSize) 80 | ida_bytes.set_cmt(addr + 1, "TypeData.RecMeths.Code", 0) 81 | funcAddr = GetCustomWord(addr + 1, self.__processorWordSize) 82 | 83 | # Name 84 | MakeStr_PASCAL(addr + 1 + self.__processorWordSize) 85 | nameSize = Byte(addr + 1 + self.__processorWordSize) 86 | funcName = GetStr_PASCAL(addr + 1 + self.__processorWordSize) 87 | ida_bytes.set_cmt( 88 | addr + 1 + self.__processorWordSize, 89 | "TypeData.RecMeths.Name", 90 | 0 91 | ) 92 | 93 | if len(funcName) == 0: 94 | funcName = hex(funcAddr) 95 | 96 | MakeName(addr, self.__typeName + "_RecMeth_" + funcName) 97 | 98 | addr += 2 + self.__processorWordSize + nameSize 99 | # ProcedureSignature 100 | # Flags 101 | MakeByte(addr) 102 | ida_bytes.set_cmt( 103 | addr, 104 | "TypeData.RecMeths.ProcedureSignature.Flags", 105 | 0 106 | ) 107 | 108 | funcRetType = "NoType" 109 | funcParamList = list() 110 | 111 | if Byte(addr) != 0xFF: 112 | # CC 113 | MakeByte(addr + 1) 114 | ida_bytes.set_cmt( 115 | addr + 1, 116 | "TypeData.RecMeths.ProcedureSignature.CC", 117 | 0 118 | ) 119 | 120 | # ResultType 121 | typeInfoAddr = GetCustomWord( 122 | addr + 2, 123 | self.__processorWordSize 124 | ) 125 | MakeCustomWord(addr + 2, self.__processorWordSize) 126 | ida_bytes.set_cmt( 127 | addr + 2, 128 | "TypeData.RecMeths.ProcedureSignature.ResultType", 129 | 0 130 | ) 131 | 132 | if typeInfoAddr and \ 133 | ida_bytes.is_mapped(typeInfoAddr) and \ 134 | ida_bytes.is_loaded(typeInfoAddr): 135 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 136 | typeInfoAddr += self.__processorWordSize 137 | typeInfo = TypeInfo(self.__delphiVersion, typeInfoAddr) 138 | typeInfo.MakeTable(1) 139 | funcRetType = typeInfo.GetTypeName() 140 | 141 | # ParamCnt 142 | MakeByte(addr + 2 + self.__processorWordSize) 143 | paramCnt = Byte(addr + 2 + self.__processorWordSize) 144 | ida_bytes.set_cmt( 145 | addr + 2 + self.__processorWordSize, 146 | "TypeData.RecMeths.ProcedureSignature.ParamCnt", 147 | 0 148 | ) 149 | 150 | addr += 3 + self.__processorWordSize 151 | 152 | # Params 153 | for i in range(paramCnt): 154 | addr, funcParam = self.__CreateProcedureParam(addr) 155 | funcParamList.append(funcParam) 156 | else: 157 | addr += 1 158 | 159 | # AttrData 160 | MakeWord(addr) 161 | ida_bytes.set_cmt( 162 | addr, 163 | "TypeData.RecMeths.ProcedureSignature.AttrData", 164 | 0 165 | ) 166 | addr += Word(addr) 167 | 168 | self.__ProcessRecordMethod( 169 | funcAddr, 170 | funcName, 171 | funcRetType, 172 | funcParamList 173 | ) 174 | 175 | return addr 176 | 177 | def __ProcessRecordMethod( 178 | self, 179 | funcAddr: int, 180 | funcName: str, 181 | funcRetType: str, 182 | funcParamList: list[(str, str)]) -> None: 183 | if funcAddr == 0 or \ 184 | ida_name.get_name(funcAddr).startswith("sub_nullsub") or \ 185 | ida_name.get_name(funcAddr).startswith(self.__typeName): 186 | return 187 | 188 | if Byte(funcAddr) == 0: 189 | MakeName(funcAddr, "sub_nullsub") 190 | else: 191 | MakeFunction(funcAddr) 192 | 193 | funcFullName = self.__typeName + "_" + funcName 194 | MakeName(funcAddr, funcFullName) 195 | 196 | if len(funcParamList): 197 | functionCmt = self.__typeName + "::" + funcName + "(" 198 | for funcType, funcName in funcParamList: 199 | functionCmt += funcType + " " + funcName + "," 200 | functionCmt = functionCmt[:-1] + ")" 201 | else: 202 | functionCmt = self.__typeName + "::" + funcName + "()" 203 | 204 | if funcRetType != "NoType": 205 | functionCmt += ":" + funcRetType 206 | 207 | ida_funcs.set_func_cmt( 208 | funcAddr, 209 | functionCmt, 210 | 1 211 | ) 212 | 213 | funcPrototype = "void __usercall " + FixName(funcFullName) + "(" 214 | for i in range(len(funcParamList)): 215 | funcPrototype += ("void* " 216 | + funcParamList[i][0] 217 | + "_" 218 | + funcParamList[i][1] 219 | + GetParamRegister(i)) 220 | 221 | if i != len(funcParamList) - 1: 222 | funcPrototype += ", " 223 | 224 | funcPrototype += ");" 225 | 226 | idc.SetType(funcAddr, funcPrototype) 227 | 228 | def __CreateProcedureParam(self, addr: int) -> (int, (str, str)): 229 | # Flags 230 | MakeByte(addr) 231 | ida_bytes.set_cmt( 232 | addr, 233 | "TypeData.RecMeths.ProcedureSignature.ProcedureParam.Flags", 234 | 0 235 | ) 236 | 237 | # ParamType 238 | typeInfoAddr = GetCustomWord(addr + 1, self.__processorWordSize) 239 | MakeCustomWord(addr + 1, self.__processorWordSize) 240 | ida_bytes.set_cmt( 241 | addr + 1, 242 | "TypeData.RecMeths.ProcedureSignature.ProcedureParam.ParamType", 243 | 0 244 | ) 245 | 246 | paramType = "NoType" 247 | if typeInfoAddr and \ 248 | ida_bytes.is_mapped(typeInfoAddr) and \ 249 | ida_bytes.is_loaded(typeInfoAddr): 250 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 251 | typeInfoAddr += self.__processorWordSize 252 | typeInfo = TypeInfo(self.__delphiVersion, typeInfoAddr) 253 | typeInfo.MakeTable(1) 254 | paramType = typeInfo.GetTypeName() 255 | 256 | # Name 257 | MakeStr_PASCAL(addr + 1 + self.__processorWordSize) 258 | nameSize = Byte(addr + 1 + self.__processorWordSize) 259 | paramName = GetStr_PASCAL(addr + 1 + self.__processorWordSize) 260 | ida_bytes.set_cmt( 261 | addr + 1 + self.__processorWordSize, 262 | "TypeData.RecMeths.ProcedureSignature.ProcedureParam.Name", 263 | 0 264 | ) 265 | 266 | # AttrData 267 | MakeWord(addr + 2 + self.__processorWordSize + nameSize) 268 | ida_bytes.set_cmt( 269 | addr + 2 + self.__processorWordSize + nameSize, 270 | "TypeData.RecMeths.ProcedureSignature.ProcedureParam.AttrData", 271 | 0 272 | ) 273 | 274 | if len(paramName) == 0: 275 | paramName = "UnknownParam" 276 | 277 | return addr + 4 + self.__processorWordSize + nameSize, (paramType, paramName) 278 | 279 | def __CreateManagedFields(self, addr: int) -> int: 280 | MakeDword(addr) 281 | ida_bytes.set_cmt(addr, "TypeData.ManagedFieldCount", 0) 282 | managedFieldCount = Dword(addr) 283 | addr += 4 284 | 285 | if managedFieldCount: 286 | ida_bytes.set_cmt(addr, "TypeData.ManagedFields", 0) 287 | 288 | for i in range(managedFieldCount): 289 | typeInfoAddr = GetCustomWord(addr, self.__processorWordSize) 290 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 291 | typeInfo = TypeInfo(self.__delphiVersion) 292 | typeInfo.ResolveTypeInfo(typeInfoAddr, True) 293 | 294 | MakeCustomWord(addr, self.__processorWordSize) 295 | MakeCustomWord( 296 | addr + self.__processorWordSize, 297 | self.__processorWordSize 298 | ) 299 | addr += 2 * self.__processorWordSize 300 | 301 | return addr 302 | 303 | def __CreateRecordTypeField(self, addr: int) -> int: 304 | MakeDword(addr) 305 | ida_bytes.set_cmt(addr, "TypeData.RecFldCnt", 0) 306 | recFldCnt = Dword(addr) 307 | addr += 4 308 | 309 | for i in range(recFldCnt): 310 | typeInfoAddr = GetCustomWord(addr, self.__processorWordSize) 311 | from DelphiHelper.core.DelphiClass_TypeInfo import TypeInfo 312 | typeInfo = TypeInfo(self.__delphiVersion) 313 | typeInfo.ResolveTypeInfo(typeInfoAddr, True) 314 | 315 | # TypeRef 316 | MakeCustomWord(addr, self.__processorWordSize) 317 | ida_bytes.set_cmt(addr, "TypeData.RecField.TypeRef", 0) 318 | 319 | # FldOffset 320 | MakeCustomWord( 321 | addr + self.__processorWordSize, 322 | self.__processorWordSize 323 | ) 324 | ida_bytes.set_cmt( 325 | addr + self.__processorWordSize, 326 | "TypeData.RecField.FldOffset", 327 | 0 328 | ) 329 | 330 | # Flags 331 | MakeByte(addr + 2 * self.__processorWordSize) 332 | ida_bytes.set_cmt( 333 | addr + 2 * self.__processorWordSize, 334 | "TypeData.RecField.Flags", 335 | 0 336 | ) 337 | 338 | # Name 339 | MakeStr_PASCAL(addr + 2 * self.__processorWordSize + 1) 340 | fieldName = GetStr_PASCAL(addr + 2 * self.__processorWordSize + 1) 341 | fieldNameSize = Byte(addr + 2 * self.__processorWordSize + 1) 342 | ida_bytes.set_cmt( 343 | addr + 2 * self.__processorWordSize + 1, 344 | "TypeData.RecField.Name", 345 | 0 346 | ) 347 | 348 | # AttrData 349 | MakeWord(addr + 2 * self.__processorWordSize + 2 + fieldNameSize) 350 | attrDataSize = Word(addr + 2 * self.__processorWordSize + 2 + fieldNameSize) 351 | ida_bytes.set_cmt( 352 | addr + 2 * self.__processorWordSize + 2 + fieldNameSize, 353 | "TypeData.RecField.AttrData", 354 | 0 355 | ) 356 | 357 | if len(fieldName) == 0: 358 | fieldName = hex(addr) 359 | 360 | MakeName(addr, self.__typeName + "_RecField_" + fieldName) 361 | addr += 2 * self.__processorWordSize + 2 + fieldNameSize + attrDataSize 362 | 363 | return addr 364 | 365 | def DeleteTypeData(self) -> None: 366 | managedFieldsSize = 0 367 | if Dword(self.__typeDataAddr + 4): 368 | managedFieldsSize = Dword(self.__typeDataAddr + 4) * 2 * self.__processorWordSize 369 | 370 | addr = self.__typeDataAddr + 8 + managedFieldsSize 371 | 372 | if self.__delphiVersion != -1 and self.__delphiVersion >= 2010: 373 | numOps = Byte(addr) 374 | addr += 1 + numOps * self.__processorWordSize + 4 375 | 376 | for i in range(Dword(addr - 4)): 377 | fieldNameSize = Byte(addr + 2 * self.__processorWordSize + 1) 378 | attrDataSize = Word(addr + 2 * self.__processorWordSize + 2 + fieldNameSize) 379 | addr += 2 * self.__processorWordSize + 2 + fieldNameSize + attrDataSize 380 | 381 | addr += Word(addr) 382 | 383 | if self.__delphiVersion >= 2012: 384 | recMethCnt = Word(addr) 385 | addr += 2 386 | 387 | if recMethCnt and recMethCnt <= 4096: 388 | for i in range(recMethCnt): 389 | addr += 1 + self.__processorWordSize 390 | addr += 2 + Byte(addr) 391 | 392 | if Byte(addr - 1) != 0xFF: 393 | addr += 2 + self.__processorWordSize 394 | for y in range(Byte(addr - 1)): 395 | addr += 1 + self.__processorWordSize 396 | addr += 3 + Byte(addr) 397 | addr += Word(addr) 398 | 399 | ida_bytes.del_items( 400 | self.__typeDataAddr, 401 | ida_bytes.DELIT_DELNAMES, 402 | addr - self.__typeDataAddr 403 | ) 404 | -------------------------------------------------------------------------------- /DelphiHelper/core/IDRKBParser.py: -------------------------------------------------------------------------------- 1 | # 2 | # This module allows to parse IDR Knowledge base files and to determine 3 | # Delphi's version. 4 | # 5 | # The module is based on code from IDR project 6 | # (https://github.com/crypto2011/IDR), 7 | # which is licensed under the MIT License. 8 | # 9 | 10 | """ 11 | MIT License 12 | 13 | Copyright (c) 2006-2018 crypto 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | 34 | 35 | import ida_idaapi 36 | import ida_kernwin 37 | import struct 38 | from DelphiHelper.util.delphi import * 39 | from DelphiHelper.util.exception import DelphiHelperError 40 | from DelphiHelper.util.ida import * 41 | 42 | 43 | class KBParser(object): 44 | 45 | def __init__(self, KB_filename: str) -> None: 46 | self.__moduleOffsets = list() 47 | self.__constOffsets = list() 48 | self.__typeOffsets = list() 49 | self.__varOffsets = list() 50 | self.__resStrOffsets = list() 51 | self.__procOffsets = list() 52 | 53 | self.__kbFile = open(KB_filename, "rb") 54 | 55 | if not self.__CheckKBFile(): 56 | self.__kbFile.close() 57 | raise DelphiHelperError("Unexpected Knowledge base file format.") 58 | 59 | self.__kbFile.seek(-4, 2) 60 | sectionsOffset = struct.unpack("i", self.__kbFile.read(4))[0] 61 | self.__kbFile.seek(sectionsOffset, 0) 62 | 63 | self.__moduleCount = struct.unpack("i", self.__kbFile.read(4))[0] 64 | self.__maxModuleDataSize = struct.unpack("i", self.__kbFile.read(4))[0] 65 | 66 | for i in range(self.__moduleCount): 67 | self.__moduleOffsets.append(self.__ReadOffetsInfo()) 68 | 69 | self.__constCount = struct.unpack("i", self.__kbFile.read(4))[0] 70 | self.__maxConstDataSize = struct.unpack("i", self.__kbFile.read(4))[0] 71 | 72 | for i in range(self.__constCount): 73 | self.__constOffsets.append(self.__ReadOffetsInfo()) 74 | 75 | self.__typeCount = struct.unpack("i", self.__kbFile.read(4))[0] 76 | self.__maxTypeDataSize = struct.unpack("i", self.__kbFile.read(4))[0] 77 | 78 | for i in range(self.__typeCount): 79 | self.__typeOffsets.append(self.__ReadOffetsInfo()) 80 | 81 | self.__varCount = struct.unpack("i", self.__kbFile.read(4))[0] 82 | self.__maxVarDataSize = struct.unpack("i", self.__kbFile.read(4))[0] 83 | 84 | for i in range(self.__varCount): 85 | self.__varOffsets.append(self.__ReadOffetsInfo()) 86 | 87 | self.__resStrCount = struct.unpack("i", self.__kbFile.read(4))[0] 88 | self.__maxResStrDataSize = struct.unpack("i", self.__kbFile.read(4))[0] 89 | 90 | for i in range(self.__resStrCount): 91 | self.__resStrOffsets.append(self.__ReadOffetsInfo()) 92 | 93 | self.__procCount = struct.unpack("i", self.__kbFile.read(4))[0] 94 | self.__maxProcDataSize = struct.unpack("i", self.__kbFile.read(4))[0] 95 | 96 | for i in range(self.__procCount): 97 | self.__procOffsets.append(self.__ReadOffetsInfo()) 98 | 99 | self.__kbFile.seek(0, 0) 100 | self.__kbMem = bytes() 101 | self.__kbMem = self.__kbFile.read() 102 | self.__kbFile.close() 103 | 104 | def __ReadOffetsInfo(self) -> list: 105 | offsetsInfo = list() 106 | offsetsInfo.append(struct.unpack("I", self.__kbFile.read(4))[0]) 107 | offsetsInfo.append(struct.unpack("I", self.__kbFile.read(4))[0]) 108 | offsetsInfo.append(struct.unpack("i", self.__kbFile.read(4))[0]) 109 | offsetsInfo.append(struct.unpack("i", self.__kbFile.read(4))[0]) 110 | 111 | return offsetsInfo 112 | 113 | def __CheckKBFile(self) -> bool: 114 | signature = self.__kbFile.read(24) 115 | self.__kbFile.seek(265, 1) 116 | kbVersion = struct.unpack("i", self.__kbFile.read(4))[0] 117 | 118 | if (signature == b"IDD Knowledge Base File\x00") and (kbVersion == 1): 119 | self.__version = kbVersion 120 | return True 121 | 122 | if (signature == b"IDR Knowledge Base File\x00") and (kbVersion == 2): 123 | self.__version = kbVersion 124 | return True 125 | 126 | return False 127 | 128 | def GetFunctions( 129 | self, 130 | firstIndex: int = -1, 131 | lastIndex: int = -1) -> list[tuple[str, str]]: 132 | functions = list() 133 | 134 | if firstIndex == -1: 135 | firstIndex = 0 136 | 137 | if lastIndex == -1: 138 | lastIndex = self.__procCount - 1 139 | 140 | for i in range(firstIndex, lastIndex + 1, 1): 141 | procInfo = self.GetProcInfo(i) 142 | 143 | if procInfo["DumpSz"] > 9: 144 | code = str() 145 | 146 | for y in range(procInfo["DumpSz"]): 147 | b = procInfo["Dump"][y] 148 | 149 | if procInfo["Reloc"][y] == 0xff: 150 | code += "?? " 151 | elif b < 0x10: 152 | code += "0" + hex(b)[2:] + " " 153 | else: 154 | code += hex(b)[2:] + " " 155 | 156 | functions.append((procInfo["ProcName"].decode("utf-8"), code[:-1])) 157 | 158 | return functions 159 | 160 | def GetModuleID(self, moduleName: str) -> int: 161 | if moduleName == "" or self.__moduleCount == 0: 162 | return -1 163 | 164 | L = 0 165 | R = self.__moduleCount - 1 166 | 167 | while L < R: 168 | M = (L + R) // 2 169 | ID = self.__moduleOffsets[M][3] # NameID 170 | nameLen = struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0] + 2: self.__moduleOffsets[ID][0] + 4])[0] 171 | name = self.__kbMem[self.__moduleOffsets[ID][0] + 4: self.__moduleOffsets[ID][0] + 4 + nameLen].decode("utf-8") 172 | 173 | if moduleName.lower() <= name.lower(): 174 | R = M 175 | else: 176 | L = M + 1 177 | 178 | ID = self.__moduleOffsets[R][3] 179 | nameLen = struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0] + 2: self.__moduleOffsets[ID][0] + 4])[0] 180 | name = self.__kbMem[self.__moduleOffsets[ID][0] + 4: self.__moduleOffsets[ID][0] + 4 + nameLen].decode("utf-8") 181 | 182 | if moduleName == name: 183 | return struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0]: self.__moduleOffsets[ID][0] + 2])[0] 184 | 185 | return -1 186 | 187 | def GetProcIdx(self, moduleID: int, procName: str) -> int: 188 | """Return proc index by name in given ModuleID""" 189 | 190 | if moduleID == -1 or procName == "" or self.__procCount == 0: 191 | return -1 192 | 193 | L = 0 194 | R = self.__procCount - 1 195 | 196 | while True: 197 | M = (L + R) // 2 198 | # NameID 199 | ID = self.__procOffsets[M][3] 200 | nameLen = struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0] + 2: self.__moduleOffsets[ID][0] + 4])[0] 201 | name = self.__kbMem[self.__procOffsets[ID][0] + 4: self.__procOffsets[ID][0] + 4 + nameLen].decode("utf-8") 202 | 203 | if procName < name: 204 | R = M - 1 205 | elif procName > name: 206 | L = M + 1 207 | else: 208 | # Find left boundary 209 | LN = M - 1 210 | while LN >= 0: 211 | ID = self.__procOffsets[LN][3] # NameID 212 | nameLen = struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0] + 2: self.__moduleOffsets[ID][0] + 4])[0] 213 | name = self.__kbMem[self.__procOffsets[ID][0] + 4: self.__procOffsets[ID][0] + 4 + nameLen].decode("utf-8") 214 | 215 | if procName != name: 216 | break 217 | 218 | LN -= 1 219 | 220 | # Find right boundary 221 | RN = M + 1 222 | 223 | while RN < self.__procCount: 224 | ID = self.__procOffsets[RN][3] # NameID 225 | nameLen = struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0] + 2: self.__moduleOffsets[ID][0] + 4])[0] 226 | name = self.__kbMem[self.__procOffsets[ID][0] + 4: self.__procOffsets[ID][0] + 4 + nameLen].decode("utf-8") 227 | 228 | if procName != name: 229 | break 230 | 231 | RN += 1 232 | 233 | N = LN + 1 234 | 235 | while N < RN: 236 | ID = self.__procOffsets[N][3] # NameID 237 | ModID = struct.unpack("H", self.__kbMem[self.__moduleOffsets[ID][0]: self.__moduleOffsets[ID][0] + 2])[0] 238 | 239 | if moduleID == ModID: 240 | return N 241 | 242 | N += 1 243 | 244 | return -1 245 | 246 | if L > R: 247 | return -1 248 | 249 | def GetProcIdxs(self, moduleID: int) -> tuple[int, int]: 250 | firstIndex = -1 251 | lastIndex = -1 252 | 253 | if moduleID != -1 or self.__procCount != 0: 254 | L = 0 255 | R = self.__procCount - 1 256 | 257 | while True: 258 | M = (L + R) // 2 259 | ID = self.__procOffsets[M][2] 260 | ModID = struct.unpack("H", self.__kbMem[self.__procOffsets[ID][0]: self.__procOffsets[ID][0] + 2])[0] 261 | 262 | if moduleID < ModID: 263 | R = M - 1 264 | elif moduleID > ModID: 265 | L = M + 1 266 | else: 267 | firstIndex = M 268 | lastIndex = M 269 | 270 | LN = M - 1 271 | while LN >= 0: 272 | ID = self.__procOffsets[LN][2] 273 | ModID = struct.unpack("H", self.__kbMem[self.__procOffsets[ID][0]: self.__procOffsets[ID][0] + 2])[0] 274 | 275 | if moduleID != ModID: 276 | break 277 | 278 | firstIndex = LN 279 | LN -= 1 280 | 281 | RN = M + 1 282 | while RN < self.__procCount: 283 | ID = self.__procOffsets[RN][2] 284 | ModID = struct.unpack("H", self.__kbMem[self.__procOffsets[ID][0]: self.__procOffsets[ID][0] + 2])[0] 285 | 286 | if moduleID != ModID: 287 | break 288 | 289 | lastIndex = RN 290 | RN += 1 291 | 292 | return firstIndex, lastIndex 293 | 294 | if L > R: 295 | return -1, -1 296 | 297 | return firstIndex, lastIndex 298 | 299 | def GetProcInfo(self, procIdx: int) -> int: 300 | 301 | if procIdx == -1: 302 | return 0 303 | 304 | procInfo = dict() 305 | rawProcInfo = self.__kbMem[self.__procOffsets[procIdx][0]: self.__procOffsets[procIdx][0] + self.__procOffsets[procIdx][1]] 306 | pos = 0 307 | 308 | procInfo["ModuleID"] = struct.unpack("H", rawProcInfo[pos: pos + 2])[0] 309 | pos += 2 310 | Len = struct.unpack("H", rawProcInfo[pos: pos + 2])[0] 311 | pos += 2 312 | procInfo["ProcName"] = rawProcInfo[pos: pos + Len]#.decode("utf-8") 313 | pos += Len + 1 314 | # procInfo["Embedded"] = rawProcInfo[pos] 315 | pos += 1 316 | # procInfo["DumpType"] = rawProcInfo[pos] 317 | pos += 1 318 | # procInfo["MethodKind"] = rawProcInfo[pos] 319 | pos += 1 320 | # procInfo["CallKind"] = rawProcInfo[pos] 321 | pos += 1 322 | # procInfo["VProc"] = struct.unpack("I", rawProcInfo[pos : pos + 4])[0] 323 | pos += 4 324 | Len = struct.unpack("H", rawProcInfo[pos: pos + 2])[0] 325 | pos += 2 + Len + 1 326 | 327 | # pInfo->TypeDef = TrimTypeName(String((char*)p)); p += Len + 1; 328 | 329 | # dumpTotal = struct.unpack("I", rawProcInfo[pos : pos + 4])[0] 330 | pos += 4 331 | procInfo["DumpSz"] = struct.unpack("I", rawProcInfo[pos: pos + 4])[0] 332 | pos += 4 333 | # procInfo["FixupNum"] = struct.unpack("I", rawProcInfo[pos : pos + 4])[0] 334 | pos += 4 335 | procInfo["Dump"] = b"" 336 | if procInfo["DumpSz"]: 337 | procInfo["Dump"] = rawProcInfo[pos: pos + procInfo["DumpSz"]] 338 | procInfo["Reloc"] = rawProcInfo[pos + procInfo["DumpSz"]: pos + 2*procInfo["DumpSz"]] 339 | return procInfo 340 | 341 | 342 | # 32bit 343 | STRINGCOPY_2014_PATTERN = "53 85 d2 75 07 66 c7 00 00 00 eb 26 85 d2 7e 16 0f b7 19 66 89 18 66 85 db 74 17 83 c0 02 83 c1 02 4a 85 d2 7f ea 85 d2 75 08 83 e8 02 66 c7 00 00 00 5b c3" 344 | ERRORAT_2014_PATTERN = "53 56 8b f2 8b d8 80 e3 7f 83 3d ?? ?? ?? ?? 00 74 0a 8b d6 8b c3 ff 15 ?? ?? ?? ?? 84 db 75 0d e8 ?? ?? ?? ?? 8b 98 ?? ?? ?? ?? eb 0f 80 fb 1c 77 0a 0f b6 c3 0f b6 98 ?? ?? ?? ?? 0f b6 c3 8b d6 e8 ?? ?? ?? ?? 5e 5b c3" 345 | FINALIZERESSTRINGS_2013_PATTERN = "53 31 db 57 56 8b 3c 18 8d 74 18 04 8b 06 01 da 8b 4e 08 85 c9 74 07 49 74 0b 49 74 0f cc e8 ?? ?? ?? ?? eb 0c e8 ?? ?? ?? ?? eb 05 e8 ?? ?? ?? ?? 83 c6 0c 4f 75 d5 5e 5f 5b c3" 346 | OBJECT_FIELDADDRESS_2013_PATTERN = "53 56 57 31 c9 31 ff 8a 1a 50 8b 00 8b 70 bc 85 f6 74 18 66 8b 3e 85 ff 74 11 83 c6 06 8a 4e 06 38 d9 74 2c 8d 74 31 07 4f 75 f2 8b 40 d0 85 c0 75 d8 5a eb 39 8a 1a 8a 4e 06 eb e8 50 52 8d 46 06 e8 ?? ?? ?? ?? 31 c9 84 c0 5a 58 74 e7 eb 19 8a 5c 31 06 f6 c3 80 75 e3 32 1c 11 f6 c3 80 75 db 80 e3 df 75 cf 49 75 e7 8b 06 5a 01 d0 5f 5e 5b c3" 347 | INITIALIZECONTROLWORD_2012_PATTERN = "e8 ?? ?? ?? ?? a3 ?? ?? ?? ?? e8 ?? ?? ?? ?? 25 c0 ff 00 00 a3 ?? ?? ?? ?? c3" 348 | ERRORAT_2011_PATTERN = "53 56 8b f2 8b d8 80 e3 7f 83 3d ?? ?? ?? ?? 00 74 0a 8b d6 8b c3 ff 15 ?? ?? ?? ?? 84 db 75 0d e8 ?? ?? ?? ?? 8b 98 ?? ?? ?? ?? eb 0f 80 fb 1b 77 0a 0f b6 c3 0f b6 98 ?? ?? ?? ?? 0f b6 c3 8b d6 e8 ?? ?? ?? ?? 5e 5b c3" 349 | ERRORAT_2010_PATTERN = "53 56 8b f2 8b d8 80 e3 7f 83 3d ?? ?? ?? ?? 00 74 0a 8b d6 8b c3 ff 15 ?? ?? ?? ?? 84 db 75 0d e8 ?? ?? ?? ?? 8b 98 ?? ?? ?? ?? eb 0f 80 fb 1a 77 0a 0f b6 c3 0f b6 98 ?? ?? ?? ?? 0f b6 c3 8b d6 e8 ?? ?? ?? ?? 5e 5b c3" 350 | 351 | 352 | def getDelphiVersion32() -> int: 353 | initTableAddr = IsExtendedInitTab() 354 | 355 | if initTableAddr: 356 | STRINGCOPY_addr = find_bytes(STRINGCOPY_2014_PATTERN) 357 | ERRORAT_2014_addr = find_bytes(ERRORAT_2014_PATTERN) 358 | 359 | if STRINGCOPY_addr != ida_idaapi.BADADDR or \ 360 | ERRORAT_2014_addr != ida_idaapi.BADADDR: 361 | print("[INFO] Delphi version: >= 2014") 362 | return 2014 363 | 364 | FINALIZERESSTRINGS_addr = find_bytes(FINALIZERESSTRINGS_2013_PATTERN) 365 | OBJECT_FIELDADDRESS_addr = find_bytes(OBJECT_FIELDADDRESS_2013_PATTERN) 366 | 367 | if OBJECT_FIELDADDRESS_addr != ida_idaapi.BADADDR or \ 368 | FINALIZERESSTRINGS_addr != ida_idaapi.BADADDR: 369 | print("[INFO] Delphi version: 2013") 370 | return 2013 371 | 372 | INITIALIZECONTROLWORD_addr = find_bytes(INITIALIZECONTROLWORD_2012_PATTERN) 373 | 374 | if INITIALIZECONTROLWORD_addr != ida_idaapi.BADADDR: 375 | print("[INFO] Delphi version: 2012") 376 | return 2012 377 | 378 | ERRORAT_2011_addr = find_bytes(ERRORAT_2011_PATTERN) 379 | 380 | if ERRORAT_2011_addr != ida_idaapi.BADADDR: 381 | print("[INFO] Delphi version: 2011") 382 | return 2011 383 | 384 | ERRORAT_2010_addr = find_bytes(ERRORAT_2010_PATTERN) 385 | 386 | if ERRORAT_2010_addr != ida_idaapi.BADADDR: 387 | print("[INFO] Delphi version: 2010") 388 | return 2010 389 | 390 | print("[INFO] Delphi version: UNKNOWN") 391 | return -1 392 | 393 | print("[INFO] Delphi version: <= 2009") 394 | return 2009 395 | 396 | 397 | # 64bit 398 | STRINGCOPY_2014_PATTERN64 = "85 d2 75 07 66 c7 01 00 00 eb 2a 85 d2 7e 1b 49 0f b7 00 66 89 01 66 85 c0 74 1a 48 83 c1 02 49 83 c0 02 83 ea 01 85 d2 7f e5 90 85 d2 75 06 66 c7 41 fe 00 00 c3" 399 | ERRORAT_2014_PATTERN64 = "56 53 48 83 ec 28 89 cb 48 89 d6 80 e3 7f 48 83 3d ?? ?? ?? ?? 00 74 0b 89 d9 48 89 f2 ff 15 ?? ?? ?? ?? 84 db 75 0f e8 ?? ?? ?? ?? 48 0f b6 98 ?? ?? ?? ?? eb 15 80 fb 1c 77 10 48 8d 05 ?? ?? ?? ?? 48 0f b6 db 48 0f b6 1c 18 48 0f b6 cb 48 89 f2 e8 ?? ?? ?? ?? 48 83 c4 28 5b 5e c3" 400 | OBJECT_CLASSNAME_2013_PATTERN64 = "53 48 83 ec 20 48 89 d3 48 89 d8 48 8b 91 78 ff ff ff 48 89 c1 e8 ?? ?? ?? ?? 48 89 d8 48 83 c4 20 5b c3" 401 | CLASSCREATE_2013_PATTERN64 = "48 83 ec 28 48 89 c8 84 d2 7c 06 48 89 c1 ff 50 d0 48 83 c4 28 c3" 402 | INITIALIZECONTROLWORD_2012_PATTERN64 = "48 83 ec 28 c7 05 ?? ?? ?? ?? 03 00 00 00 e8 ?? ?? ?? ?? 66 81 e0 3f 1f 66 89 05 ?? ?? ?? ?? 48 83 c4 28 c3" 403 | 404 | 405 | def getDelphiVersion64() -> int: 406 | STRINGCOPY_addr = find_bytes(STRINGCOPY_2014_PATTERN64) 407 | ERRORAT_addr = find_bytes(ERRORAT_2014_PATTERN64) 408 | if ERRORAT_addr != ida_idaapi.BADADDR or \ 409 | STRINGCOPY_addr != ida_idaapi.BADADDR: 410 | print("[INFO] Delphi version: >= 2014") 411 | return 2014 412 | 413 | OBJECT_CLASSNAME_addr = find_bytes(OBJECT_CLASSNAME_2013_PATTERN64) 414 | CLASSCREATE_addr = find_bytes(CLASSCREATE_2013_PATTERN64) 415 | 416 | if OBJECT_CLASSNAME_addr != ida_idaapi.BADADDR or \ 417 | CLASSCREATE_addr != ida_idaapi.BADADDR: 418 | print("[INFO] Delphi version: 2013") 419 | return 2013 420 | 421 | INITIALIZECONTROLWORD_addr = find_bytes(INITIALIZECONTROLWORD_2012_PATTERN64) 422 | 423 | if INITIALIZECONTROLWORD_addr != ida_idaapi.BADADDR: 424 | print("[INFO] Delphi version: 2012") 425 | return 2012 426 | 427 | print("[INFO] Delphi version: UNKNOWN") 428 | return 2014 429 | 430 | 431 | def GetDelphiVersion() -> int: 432 | ida_kernwin.show_wait_box("Trying to determine Delphi version...") 433 | try: 434 | if Is64bit(): 435 | return getDelphiVersion64() 436 | else: 437 | return getDelphiVersion32() 438 | finally: 439 | ida_kernwin.hide_wait_box() 440 | --------------------------------------------------------------------------------