script changes

This commit is contained in:
EricPlayZ
2025-03-31 03:45:50 +03:00
parent 970f5745aa
commit ef14ed13c5
14 changed files with 11707 additions and 436 deletions

3
.gitignore vendored
View File

@ -1,5 +1,5 @@
_IDAScripts/.env
_IDAScripts/_generated/
_IDAScripts/generated/*.json
# Ultimate-ASI-Loader
data/
@ -408,3 +408,4 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
/_IDAScripts/generated/parsed-classes.json

View File

@ -157,7 +157,7 @@
</OptimizeReferences>
<EnableCOMDATFolding>
</EnableCOMDATFolding>
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;</AdditionalLibraryDirectories>
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;$(SolutionDir)$(Platform)\$(Configuration)\;</AdditionalLibraryDirectories>
<AssemblyDebug>true</AssemblyDebug>
<ImportLibrary />
<FixedBaseAddress>
@ -192,7 +192,7 @@
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;$(SolutionDir)$(Platform)\$(Configuration)\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>DbgHelp.lib;Version.lib;EGameSDK.lib;libMinHook-x64-v141-mdd.lib;engine_x64_rwdi.lib;freetype-md.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
<ImportLibrary />
<AdditionalOptions>/NOIMPLIB /NOEXP %(AdditionalOptions)</AdditionalOptions>

View File

@ -1,5 +1,5 @@
{
"PROJECT_INCLUDES_PATH": "D:\\PROJECTS\\Visual Studio\\EGameSDK\\EGameSDK\\include",
"OUTPUT_PATH": "D:\\PROJECTS\\Visual Studio\\EGameSDK\\_IDAScripts",
"LAST_CLICKED_RADIO": 1
"LAST_CLICKED_RADIO": 2
}

View File

@ -1,63 +1,54 @@
from typing import Optional, List, Dict, get_type_hints
from prodict import Prodict
class ParsedParam(Prodict):
type: str
name: str
parsedClassParam: Optional["ParsedClass"]
from __future__ import annotations
from typing import Optional, List
from pydantic import BaseModel, SkipValidation
def init(self):
self.type = ""
self.name = ""
self.parsedClassParam = None
def DefaultPydanticSerializer(obj):
if hasattr(obj, "model_dump"):
return obj.model_dump(exclude_none=True)
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
class ParsedClass(Prodict):
type: str
parentNamespaces: List[str]
parentClasses: List[str]
name: str
templateParams: List["ParsedParam"]
fullClassName: str
childClasses: Prodict
functions: List["ParsedFunction"]
class ParsedParam(BaseModel):
type: str = ""
name: str = ""
parsedClassParam: Optional[SkipValidation[ParsedClass]] = None
def init(self):
self.type = ""
self.parentNamespaces = []
self.parentClasses = []
self.name = ""
self.templateParams = []
self.fullClassName = ""
self.childClasses = Prodict()
self.functions = []
class ParsedClass(BaseModel):
type: str = ""
parentNamespaces: List[str] = []
parentClasses: List[str] = []
classDependencies: List[str] = []
name: str = ""
templateParams: List[ParsedParam] = []
fullClassName: str = ""
childClasses: dict = {}
classVars: List[ParsedClassVar] = []
virtualFunctions: List[ParsedFunction] = []
functions: List[ParsedFunction] = []
class ParsedFunction(Prodict):
type: str
funcType: str
access: str
returnTypes: List[ParsedParam]
parentNamespaces: List[str]
parentClasses: List[str]
fullClassName: str
funcName: str
params: List[ParsedParam]
const: bool
fullFuncSig: str
class ParsedFunction(BaseModel):
type: str = "function"
funcType: str = "function"
access: str = "public"
returnTypes: List[ParsedParam] = []
parentNamespaces: List[str] = []
parentClasses: List[str] = []
fullClassName: str = ""
funcName: str = ""
params: List[ParsedParam] = []
const: bool = False
fullFuncSig: str = ""
def init(self):
self.type = "function"
self.funcType = "function"
self.access = "public"
self.returnTypes = []
self.parentNamespaces = []
self.parentClasses = []
self.fullClassName = ""
self.funcName = ""
self.params = []
self.const = False
self.fullFuncSig = ""
ParsedParam.__annotations__ = get_type_hints(ParsedParam)
ParsedParam.__annotations__["parsedClassParam"] = ParsedClass
ParsedClass.__annotations__ = get_type_hints(ParsedClass)
ParsedFunction.__annotations__ = get_type_hints(ParsedFunction)
class ParsedClassVar(BaseModel):
type: str = "classVar"
access: str = "public"
varTypes: List[ParsedParam] = []
parentNamespaces: List[str] = []
parentClasses: List[str] = []
fullClassName: str = ""
varName: str = ""
fullClassVarSig: str = ""
ParsedParam.model_rebuild()
ParsedClass.model_rebuild()
ParsedFunction.model_rebuild()
ParsedClassVar.model_rebuild()

View File

@ -5,15 +5,13 @@ INTERNAL_SCRIPT_NAME = "ExportClassH"
PROJECT_INCLUDES_PATH = r"D:\PROJECTS\Visual Studio\EGameSDK\EGameSDK\include"
OUTPUT_PATH = r"D:\PROJECTS\Visual Studio\EGameSDK\_IDAScripts"
HEADER_OUTPUT_PATH = os.path.join(OUTPUT_PATH, "generated")
CACHE_OUTPUT_PATH = os.path.join(OUTPUT_PATH, "cache")
PARSED_VARS_CACHE_FILENAME = os.path.join(CACHE_OUTPUT_PATH, "parsedClassVarsByClass.cache")
PARSED_FUNCS_CACHE_FILENAME = os.path.join(CACHE_OUTPUT_PATH, "parsedFuncsByClass.cache")
INPUT_MD5 = bytes()
LAST_CLICKED_RADIO = 0
DEFAULT_CONFIG = {
"PROJECT_INCLUDES_PATH": PROJECT_INCLUDES_PATH,
"OUTPUT_PATH": OUTPUT_PATH,
"LAST_CLICKED_RADIO": LAST_CLICKED_RADIO,
"LAST_CLICKED_RADIO": LAST_CLICKED_RADIO
}
CONFIG_FILE = os.path.join(os.path.join(os.path.dirname(__file__), os.pardir), "ExportClassH.json")
PARSED_CLASSES_OUTPUT_FILE = os.path.join(HEADER_OUTPUT_PATH, "parsed-classes.json")

View File

@ -1,40 +1,39 @@
import os
from ExportClassH import ClassGen, Utils, Config
from ExportClassH.ClassDefs import ClassName, ParsedFunction, ParsedClassVar
processedClasses: set[str] = set()
from ExportClassH import JSONGen, Utils, Config
from ExportClassH.ClassDefs import ParsedClass, ParsedFunction, ParsedClassVar
currentAccess: str = "public"
def GenerateClassVarCode(classVar: ParsedClassVar) -> list[str]:
def GenerateClassVarCode(classVar: ParsedClassVar, indentLevel: str) -> list[str]:
"""Generate code for a single class variable."""
global currentAccess
access: str = f"{classVar.access}:" if classVar.access else ""
if currentAccess == classVar.access:
access = "\t"
access = ""
else:
currentAccess = classVar.access
if classVar.varType:
varType: str = Utils.ReplaceIDATypes(classVar.varType.fullName)
varType = Utils.CleanType(varType)
if varType:
varType += " "
varType = "GAME_IMPORT " + varType
if classVar.varTypes:
varTypesList: list[str] = [varType.name for varType in classVar.varTypes if varType]
varTypes: str = Utils.ReplaceIDATypes(" ".join(varTypesList))
varTypes = Utils.CleanType(varTypes)
if varTypes:
varTypes += " "
varTypes = "GAME_IMPORT " + varTypes
else:
varType: str = ""
varTypes: str = ""
classVarSig: str = f"{varType}{classVar.varName}"
classVarSig: str = f"{varTypes}{classVar.varName}"
classVarLines: list[str] = []
if access:
classVarLines.append(access)
classVarLines.append(f"{indentLevel}{access}")
if classVarSig:
classVarLines.append(f"\t{classVarSig};")
classVarLines.append(f"{indentLevel}\t{classVarSig};")
return classVarLines
def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[str]:
def GenerateClassFuncCode(func: ParsedFunction, indentLevel: str, vtFuncIndex: int = 0) -> list[str]:
"""Generate code for a single class method."""
global currentAccess
@ -45,25 +44,24 @@ def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[st
currentAccess = func.access
const: str = " const" if func.const else ""
stripped_vfunc: str = " = 0" if func.type == "stripped_vfunc" else ""
strippedVirtual: str = " = 0" if func.type == "strippedVirtual" else ""
if func.returnType:
returnType: str = Utils.ReplaceIDATypes(func.returnType.fullName)
if func.returnTypes:
returnTypesList: list[str] = [returnType.name for returnType in func.returnTypes if returnType]
returnType: str = Utils.ReplaceIDATypes(" ".join(returnTypesList))
returnType = Utils.CleanType(returnType)
if returnType:
if func.type == "basic_vfunc":
if func.type == "basicVirtual":
returnType = returnType.removeprefix("virtual").strip()
else:
returnType += " "
else:
returnType: str = ""
if func.type != "stripped_vfunc" and func.type != "basic_vfunc":
if func.type != "strippedVirtual" and func.type != "basicVirtual":
returnType = "GAME_IMPORT " + returnType
if func.params:
paramsList: list[str] = []
for param in func.params:
paramsList.append(param.fullName)
paramsList: list[str] = [param.name for param in func.params if param]
params: str = Utils.ReplaceIDATypes(", ".join(paramsList))
params = Utils.CleanType(params)
if params == "void":
@ -72,143 +70,129 @@ def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[st
params: str = ""
targetParams: str = ""
if func.type == "basic_vfunc":
targetParams = ClassGen.ExtractParamNames(params)
if func.type == "basicVirtual":
targetParams = Utils.ExtractParamNames(params)
targetParams = ", " + targetParams if targetParams else ""
funcSig: str = f"{returnType}{func.funcName}({params}){const}{stripped_vfunc}" if func.type != "basic_vfunc" else f"VIRTUAL_CALL({vtFuncIndex}, {returnType}, {func.funcName}, ({params}){targetParams})"
funcSig: str = f"{returnType}{func.funcName}({params}){const}{strippedVirtual}" if func.type != "basicVirtual" else f"VIRTUAL_CALL({vtFuncIndex}, {returnType}, {func.funcName}, ({params}){targetParams})"
classFuncLines: list[str] = []
if access:
classFuncLines.append(access)
classFuncLines.append(f"{indentLevel}{access}")
if funcSig:
classFuncLines.append(f"\t{funcSig};")
classFuncLines.append(f"{indentLevel}\t{funcSig};")
return classFuncLines
def GenerateClassContent(allParsedElements: tuple[list[ParsedClassVar], list[ParsedFunction], list[ParsedFunction]]) -> list[str]:
indentLevel = ""
def GenerateClassContent(targetClass: ParsedClass, indentLevel: str) -> list[str]:
"""
Generate the content to be inserted into an existing header file.
This is just the class members, not the full header with includes, etc.
"""
global currentAccess
parsedVars, parsedVtFuncs, parsedFuncs = allParsedElements
if not parsedVars and not parsedVtFuncs and not parsedFuncs:
return []
firstVarOrFuncAccess = ""
currentAccess = ""
# Generate class content (just the members)
contentLines = [f"#pragma region GENERATED by ExportClassToCPPH.py"]
contentLines = []
# Add class variables
for classVar in parsedVars:
for parsedClassVar in targetClass.classVars:
if not firstVarOrFuncAccess:
firstVarOrFuncAccess = classVar.access
contentLines.extend(GenerateClassVarCode(classVar))
firstVarOrFuncAccess = parsedClassVar.access
contentLines.extend(GenerateClassVarCode(parsedClassVar, indentLevel))
# Add newline between sections if both exist
if parsedVars and (parsedVtFuncs or parsedFuncs):
if targetClass.classVars and (targetClass.virtualFunctions or targetClass.functions):
contentLines.append("")
# Add vtable functions
for index, vTableFunc in enumerate(parsedVtFuncs):
for index, vTableFunc in enumerate(targetClass.virtualFunctions):
if not firstVarOrFuncAccess:
firstVarOrFuncAccess = vTableFunc.access
contentLines.extend(GenerateClassFuncCode(vTableFunc, index))
contentLines.extend(GenerateClassFuncCode(vTableFunc, indentLevel, index))
# Add newline between sections if both exist
if parsedVtFuncs and parsedFuncs:
if targetClass.virtualFunctions and targetClass.functions:
contentLines.append("")
# Add regular functions
for func in parsedFuncs:
for func in targetClass.functions:
if not firstVarOrFuncAccess:
firstVarOrFuncAccess = func.access
contentLines.extend(GenerateClassFuncCode(func))
contentLines.append("#pragma endregion")
contentLines.extend(GenerateClassFuncCode(func, indentLevel))
hasAnyContent = bool(contentLines)
# Insert access specifier if needed
if not firstVarOrFuncAccess:
contentLines.insert(1, "public:")
if not firstVarOrFuncAccess and targetClass.type == "class":
contentLines.insert(1, f"{indentLevel}public:")
childClassDefinitions: list[str] = []
for parsedClass in targetClass.childClasses.values():
classDefinition = GenerateClassDefinition(parsedClass, indentLevel + "\t")
classContent = GenerateClassContent(parsedClass, indentLevel + "\t")
childClassDefinitions.extend(classDefinition[:len(classDefinition) - 1] + classContent + classDefinition[len(classDefinition) - 1:])
childClassDefinitions.append("")
if (hasAnyContent):
contentLines.insert(0, "#pragma region GENERATED by ExportClassToCPPH.py")
contentLines[1:1] = childClassDefinitions
contentLines.append("#pragma endregion")
else:
contentLines[0:0] = childClassDefinitions
return contentLines
def GenerateClassDefinition(targetClass: ClassName, forwardDeclare: bool = False) -> list[str]:
def GenerateClassDefinition(targetClass: ParsedClass, indentLevel: str, forwardDeclare: bool = False) -> list[str]:
"""Generate a class definition from a list of methods."""
classDefLines: list[str] = [f"{targetClass.type} {targetClass.name}{' {' if not forwardDeclare else ';'}"]
if not forwardDeclare:
forwardDeclare = (targetClass.type == "enum" or targetClass.type == "union") or not targetClass.classVars and not targetClass.virtualFunctions and not targetClass.functions and targetClass.type != "namespace"
classDefLines: list[str] = [f"{indentLevel}{targetClass.type} {targetClass.name}{' {' if not forwardDeclare else ';'}"]
if not forwardDeclare:
if targetClass.type == "class":
classDefLines.append("public:")
classDefLines.append("};")
classDefLines.append(f"{indentLevel}public:")
classDefLines.append(f"{indentLevel}}};")
return classDefLines
def GenerateHeaderCode(targetClass: ClassName) -> list[str]:
def GenerateHeaderCode(targetClass: ParsedClass, parsedClassesDict: dict[str, ParsedClass]) -> list[str]:
"""Generate header code for a standard class (not nested)."""
# Reset processed classes for this generation
global processedClasses, forwardDeclarations
processedClasses = set()
forwardDeclarations = set()
global indentLevel
fullClassDefinition: list[str] = []
for namespace in targetClass.parentNamespaces:
fullClassDefinition.append(f"{indentLevel}namespace {namespace} {{")
indentLevel += "\t"
if targetClass.parentClasses:
parsedParentClass = parsedClassesDict.get(targetClass.parentClasses[0])
if parsedParentClass:
parentClassDefinition = GenerateClassDefinition(parsedParentClass, indentLevel)
parentClassContent = GenerateClassContent(parsedParentClass, indentLevel)
fullClassDefinition.extend(parentClassDefinition[:len(parentClassDefinition) - 1] + parentClassContent + parentClassDefinition[len(parentClassDefinition) - 1:])
# Mark target class as processed to avoid self-dependency issues
processedClasses.add(targetClass.namespacedClassedName)
# Get all parsed elements for the target class
allParsedElements = ClassGen.GetAllParsedClassVarsAndFuncs(targetClass)
# Generate the target class definition
classContent = GenerateClassContent(allParsedElements)
classDefinition = GenerateClassDefinition(targetClass)
classDefinition = GenerateClassDefinition(targetClass, indentLevel)
classContent = GenerateClassContent(targetClass, indentLevel)
fullClassDefinition.extend(classDefinition[:len(classDefinition) - 1] + classContent + classDefinition[len(classDefinition) - 1:])
fullClassDefinition = classDefinition
if (classContent):
fullClassDefinition = fullClassDefinition[:len(classDefinition) - 1] + classContent + fullClassDefinition[len(classDefinition) - 1:]
# Wrap target class in namespace blocks if needed
if targetClass.namespaces:
namespaceCode = []
indentLevel = ""
# Opening namespace blocks with increasing indentation
for namespace in targetClass.namespaces:
namespaceCode.append(f"{indentLevel}namespace {namespace} {{")
indentLevel += "\t"
for cls in targetClass.classes:
clsType: str = ClassGen.GetClassTypeFromParsedSigs(ClassName(targetClass.namespacedClassedName), allParsedElements)
namespaceCode.append(f"{indentLevel}{clsType if clsType else 'class'} {cls} {{")
indentLevel += "\t"
indentedClassDefinition = [f"{indentLevel if '#pragma' not in line else ''}{line}" for line in fullClassDefinition]
namespaceCode.extend(indentedClassDefinition)
for cls in reversed(targetClass.classes):
indentLevel = indentLevel[:-1] # Remove one level of indentation
namespaceCode.append(f"{indentLevel}}}")
for namespace in reversed(targetClass.namespaces):
indentLevel = indentLevel[:-1] # Remove one level of indentation
namespaceCode.append(f"{indentLevel}}}")
fullClassDefinition = namespaceCode
for namespace in reversed(targetClass.parentNamespaces):
indentLevel = indentLevel[:-1] # Remove one level of indentation
fullClassDefinition.append(f"{indentLevel}}}")
# Combine all parts of the header
headerParts = ["#pragma once", r"#include <EGSDK\Imports.h>", ""]
# Add the target class definition
headerParts.extend(fullClassDefinition)
return headerParts
def WriteHeaderToFile(targetClass: ClassName, headerCode: str, fileName: str) -> bool:
def WriteHeaderToFile(targetClass: ParsedClass, headerCode: str, fileName: str) -> bool:
"""Write the generated header code to a file."""
outputFolderPath: str = Config.HEADER_OUTPUT_PATH
if targetClass.namespaces:
if targetClass.parentNamespaces:
# Create folder structure for namespaces
classFolderPath: str = os.path.join(*targetClass.namespaces)
classFolderPath: str = os.path.join(*targetClass.parentNamespaces)
outputFolderPath: str = os.path.join(outputFolderPath, classFolderPath)
# Create directory if it doesn't exist
@ -233,26 +217,43 @@ def WriteHeaderToFile(targetClass: ClassName, headerCode: str, fileName: str) ->
print(f"Error writing header file '{outputFilePath}': {e}")
return False
def ExportClassHeader(targetClass: ClassName):
def ExportClassHeader(targetClass: str):
"""
Generate and save a C++ header file for the target class.
Handles multiple levels of nested classes and also generates dependencies.
"""
global processedClasses
"""
parsedClassesDict = JSONGen.GetAllParsedClasses()
# Skip if we've already processed this class
if targetClass.namespacedClassedName in processedClasses:
print(f"Already processed class {targetClass.namespacedClassedName}, skipping.")
parsedClass = parsedClassesDict.get(targetClass)
if not parsedClass:
print(f"There is no class {targetClass} available, therefore will not generate.")
return
# Add to processed set to prevent infinite recursion
processedClasses.add(targetClass.namespacedClassedName)
# Generate the header code
headerCodeLines = GenerateHeaderCode(targetClass)
if not headerCodeLines:
print(f"No functions were found for class {targetClass.namespacedClassedName}, therefore will not generate.")
return
JSONGen.MoveChildClasses(parsedClassesDict)
headerCodeLines = GenerateHeaderCode(parsedClass, parsedClassesDict)
headerCode: str = "\n".join(headerCodeLines)
WriteHeaderToFile(targetClass, headerCode, f"{targetClass.name}.h")
WriteHeaderToFile(parsedClass, headerCode, f"{parsedClass.name}.h")
def ExportClassHeaders():
global indentLevel
parsedClassesDict = JSONGen.GetAllParsedClasses()
JSONGen.MoveChildClasses(parsedClassesDict)
for (parsedClassName, parsedClass) in parsedClassesDict.items():
headerCodeLines = GenerateHeaderCode(parsedClass, parsedClassesDict)
headerCode: str = "\n".join(headerCodeLines)
WriteHeaderToFile(parsedClass, headerCode, f"{parsedClass.name}.h" if parsedClass.type != "namespace" else "")
# classDefinition = GenerateClassDefinition(parsedClass, indentLevel)
# classContent = GenerateClassContent(parsedClass, indentLevel)
# # Combine all parts of the header
# headerParts = ["#pragma once", r"#include <EGSDK\Imports.h>", ""]
# # Add the target class definition
# headerParts.extend(classDefinition[:len(classDefinition) - 1] + classContent + classDefinition[len(classDefinition) - 1:])
# headerCode: str = "\n".join(headerParts)
# WriteHeaderToFile(parsedClass, headerCode, f"{parsedClass.name}.h" if parsedClass.type != "namespace" else "")

View File

@ -1,7 +1,13 @@
from functools import cache
import idc
import idaapi
import idautils
import ida_bytes
import ida_nalt
from ExportClassH import Utils
IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B)
idaRTTIStrings: dict[bytes, dict[str, int]] = {}
# def GetDemangledExportedSigs() -> list[str]:
# """
@ -20,21 +26,107 @@ from ExportClassH import Utils
# sigs_set.add(demangledExportedSig)
# return list(sigs_set)
def GetDemangledExportedSigs() -> list[str]:
def GetDemangledExportedSigs(inputMD5: bytes) -> list[str]:
"""
Generate a list of demangled function signatures from IDA's database.
Uses a set to avoid duplicate entries based on (ordinal, signature).
"""
sigs_set = set()
entry_qty = idc.get_entry_qty()
for i in range(entry_qty):
ea: int = idc.get_entry(i)
sigsSet = set()
entryQty = idc.get_entry_qty()
for i in range(entryQty):
#ea: int = idc.get_entry(i)
#exportedSig: str = idc.get_func_name(ea) or idc.get_name(ea)
exportedSig = idc.get_entry_name(i)
if not exportedSig:
continue
demangledExportedSig: str = Utils.DemangleSig(exportedSig)
demangledExportedSig: str = DemangleSig(exportedSig)
if demangledExportedSig and "~" not in demangledExportedSig and not demangledExportedSig.endswith("::$TSS0") and "::`vftable'" not in demangledExportedSig:
sigs_set.add((i + 1, demangledExportedSig))
sigsSet.add((i + 1, demangledExportedSig))
return [sig for _, sig in sorted(sigs_set)]
return [sig for _, sig in sorted(sigsSet)]
@cache
def DemangleSig(sig: str) -> str:
return idaapi.demangle_name(sig, idaapi.MNG_LONG_FORM)
@cache
def GetMangledTypePrefix(namespaces: tuple[str, ...], className: str) -> str:
"""
Get the appropriate mangled type prefix for a class name.
For class "X" this would be ".?AVX@@"
For class "NS::X" this would be ".?AVX@NS@@"
For templated classes, best to use get_mangled_name_for_template instead.
"""
if not namespaces:
return f".?AV{className}@@"
# For namespaced classes, the format is .?AVClassName@Namespace@@
# For nested namespaces, they are separated with @ in reverse order
mangledNamespaces = "@".join(reversed(namespaces))
return f".?AV{className}@{mangledNamespaces}@@"
@cache
def BytesToIDAPattern(data: bytes) -> str:
"""Convert bytes to IDA-friendly hex pattern string."""
return " ".join("{:02X}".format(b) for b in data)
@cache
def GetSectionInfo(inputMD5: bytes, sectionName: str) -> tuple[int, int]:
"""Get start address and size of a specified section."""
for seg_ea in idautils.Segments():
if idc.get_segm_name(seg_ea) == sectionName:
start = seg_ea
end = idc.get_segm_end(seg_ea)
return start, end - start
return 0, 0
@cache
def GetIDARTTIStringsList(inputMD5: bytes) -> dict[str, int]:
global idaRTTIStrings
rttiStrings = idaRTTIStrings.get(inputMD5)
if not rttiStrings:
idaRTTIStrings[inputMD5] = {}
strings = idautils.Strings()
for stringItem in strings:
if not stringItem:
continue
s = str(stringItem)
if s.startswith(".?AV"):
ea = stringItem.ea if stringItem.ea else idc.BADADDR
idaRTTIStrings[inputMD5].update({ s: ea })
rttiStrings = idaRTTIStrings[inputMD5]
return rttiStrings
@cache
def FindPatternInRange(inputMD5: bytes, pattern: str, start: int, size: int, end: int = 0) -> int:
if not end:
end = start + size
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, pattern, 16, IDA_NALT_ENCODING)
if errorParsingIDAPattern:
return idc.BADADDR
patternAddr: int = ida_bytes.bin_search(start, end, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
if patternAddr == idc.BADADDR:
return idc.BADADDR
return patternAddr
@cache
def FindAllPatternsInRange(inputMD5: bytes, pattern: str, start: int, size: int, end: int = 0) -> list[int]:
"""Find all occurrences of a pattern within a memory range."""
addresses: list[int] = []
if not end:
end = start + size
while start < end:
patternAddr = FindPatternInRange(inputMD5, pattern, start, size, end)
if patternAddr == idc.BADADDR:
break
addresses.append(patternAddr)
start = patternAddr + 8 # advance past found pattern
return addresses

View File

@ -1,17 +1,20 @@
import os
import json
from typing import Optional
from functools import cache
from ExportClassH import Utils, IDAUtils, RTTIAnalyzer, Config
from ExportClassH.ClassDefs import ParsedClass, ParsedFunction, ParsedParam
from ExportClassH.ClassDefs import ParsedParam, ParsedClass, ParsedFunction, ParsedClassVar, DefaultPydanticSerializer
CLASS_TYPES = ["namespace", "class", "struct", "enum", "union"]
FUNC_TYPES = ["function", "strippedVirtual", "basicVirtual", "virtual"]
TYPES_OF_RETURN_TYPES = ["returnType", "classReturnType"]
STD_CLASSES = ["std", "rapidjson"]
parsedClassesLookupDict: dict[str, ParsedClass] = {}
parsedClassesDict: dict[str, ParsedClass] = {}
unparsedDemangledExportedSigs: list[str]= []
@cache
def GetTypeAndNameStr(fullName: str, returnFullName: bool = False) -> str:
parts = Utils.ExtractTypeTokensFromString(fullName)
if not parts:
@ -24,6 +27,7 @@ def GetTypeAndNameStr(fullName: str, returnFullName: bool = False) -> str:
return f"{parts[i]} {parts[i + 1] if not returnFullName else ' '.join(parts[i + 1:])}"
return ""
@cache
def SplitTypeFromName(fullName: str, returnFullName: bool = False) -> tuple[str, str]:
typeAndNameStr = GetTypeAndNameStr(fullName, returnFullName)
if not typeAndNameStr:
@ -32,10 +36,10 @@ def SplitTypeFromName(fullName: str, returnFullName: bool = False) -> tuple[str,
typeAndName = typeAndNameStr.split(maxsplit=1)
return typeAndName[0], typeAndName[1]
def GetParsedParamsFromList(paramsList: list[str], type: str) -> list[ParsedParam]:
def GetParsedParamsFromList(paramsList: list[str], paramType: str) -> list[ParsedParam]:
params: list[ParsedParam] = []
for i in range(len(paramsList)):
typeOfParam: str = type
typeOfParam: str = paramType
classType, className = SplitTypeFromName(paramsList[i], True)
nameOfParam: str = className
parsedClassOfParam: Optional[ParsedClass] = None
@ -43,9 +47,14 @@ def GetParsedParamsFromList(paramsList: list[str], type: str) -> list[ParsedPara
if classType:
typeOfParam = f"class{typeOfParam[0].upper()}{typeOfParam[1:]}"
parsedClassOfParam = ParseClassStr(f"{classType} {className}")
if parsedClassOfParam and parsedClassOfParam.parentNamespaces:
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parsedClassOfParam.parentNamespaces)
parsedClassOfParam.parentNamespaces = parentNamespaces
parsedClassOfParam.parentClasses = parentClasses
params.append(ParsedParam(type=typeOfParam, name=nameOfParam, parsedClassParam=parsedClassOfParam))
return params
@cache
def ExtractClassNameAndTemplateParams(templatedClassName: str) -> tuple[str, list[ParsedParam]]:
className = templatedClassName
templateParams: list[ParsedParam] = []
@ -122,7 +131,8 @@ def ParseClassStr(clsStr: str) -> Optional[ParsedClass]:
virtualFuncDuplicateCounter: dict[tuple[str, str], int] = {}
virtualFuncPlaceholderCounter: dict[tuple[str, str], int] = {}
def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVirtualFuncs: bool = False) -> Optional[ParsedFunction]:
def ParseFuncStr(funcStr: str, parsedClassFullName: str = "", onlyVirtualFuncs: bool = False) -> Optional[ParsedFunction]:
global virtualFuncDuplicateCounter
global virtualFuncPlaceholderCounter
@ -164,7 +174,8 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
# Extract parameters
paramsStr = funcStr[paramsOpenParenIndex + 1:paramsCloseParenIndex]
params = GetParsedParamsFromList(Utils.SplitByCommaOutsideTemplates(paramsStr), "param")
parsedFunc.params = params
if params and params[0] != "void":
parsedFunc.params = params
# Check for const qualifier
remainingInputAfterParamsParen = funcStr[paramsCloseParenIndex + 1:].strip()
@ -212,13 +223,13 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
parentNamespacesAndClasses = Utils.SplitByClassSeparatorOutsideTemplates(namespacesAndClasses)
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parentNamespacesAndClasses)
parsedFunc.parentNamespaces = parentNamespaces
parsedFunc.parentNamespaces = parentClasses
parsedFunc.parentClasses = parentClasses
# Handle duplicate function naming
if isDuplicateFunc:
if not parsedClass and not namespacesAndClasses:
if not parsedClassFullName and not namespacesAndClasses:
raise Exception("parsedClass variable not provided and namespacesAndClasses is empty for ParseFuncStr when func is duplicate")
key = (parsedClass.fullClassName if parsedClass else namespacesAndClasses, funcStr)
key = (parsedClassFullName if parsedClassFullName else namespacesAndClasses, funcStr)
if key not in virtualFuncDuplicateCounter:
virtualFuncDuplicateCounter[key] = 0
virtualFuncDuplicateCounter[key] += 1
@ -245,10 +256,10 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
parsedFunc.returnTypes = returnTypes
parsedFunc.funcName = funcName
elif onlyVirtualFuncs and funcStr == "_purecall":
if not parsedClass:
if not parsedClassFullName:
raise Exception("parsedClass variable not provided for ParseFuncStr when func is _purecall")
key = (parsedClass.fullClassName, funcStr)
key = (parsedClassFullName, funcStr)
if key not in virtualFuncPlaceholderCounter:
virtualFuncPlaceholderCounter[key] = 0
virtualFuncPlaceholderCounter[key] += 1
@ -258,6 +269,69 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
return parsedFunc
def ParseClassVarStr(classVarStr: str) -> Optional[ParsedClassVar]:
# Strip whitespace
classVarStr = classVarStr.strip()
if not classVarStr:
return None
parsedClassVar = ParsedClassVar()
parsedClassVar.fullClassVarSig = classVarStr
# Extract access modifier
for keyword in ("public:", "protected:", "private:"):
if classVarStr.startswith(keyword):
parsedClassVar.access = keyword[:-1] # remove the colon
classVarStr = classVarStr[len(keyword):].strip()
break
# Find parameters and const qualifier
paramsOpenParenIndex = classVarStr.find('(')
paramsCloseParenIndex = classVarStr.rfind(')')
# For class variables, we expect no parameters (i.e. no parentheses).
if paramsOpenParenIndex == -1 and paramsCloseParenIndex == -1:
varType = ""
namespacesAndClasses = ""
varName = ""
# Find the last space outside of angle brackets
lastSpaceIndex = Utils.FindLastSpaceOutsideTemplates(classVarStr)
if lastSpaceIndex != -1:
# Split at the last space outside angle brackets
varType = classVarStr[:lastSpaceIndex].strip()
classAndVarName = classVarStr[lastSpaceIndex+1:].strip()
# Find the last class separator outside of angle brackets
lastClassSeparatorIndex = Utils.FindLastClassSeparatorOutsideTemplates(classAndVarName)
if lastClassSeparatorIndex != -1:
namespacesAndClasses = classAndVarName[:lastClassSeparatorIndex]
varName = classAndVarName[lastClassSeparatorIndex+2:]
else:
classParts = Utils.SplitByClassSeparatorOutsideTemplates(classAndVarName)
namespacesAndClasses = "::".join(classParts[:-1]) if len(classParts) > 1 else ""
varName = classParts[-1]
else:
return None
parsedClassVar.fullClassName = namespacesAndClasses
parentNamespacesAndClasses = Utils.SplitByClassSeparatorOutsideTemplates(namespacesAndClasses)
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parentNamespacesAndClasses)
parsedClassVar.parentNamespaces = parentNamespaces
parsedClassVar.parentClasses = parentClasses
varType = Utils.ReplaceIDATypes(varType)
varType = Utils.CleanType(varType)
varTypes = GetParsedParamsFromList(Utils.ExtractTypeTokensFromString(varType), "classVarType")
parsedClassVar.varTypes = varTypes
parsedClassVar.varName = varName
else:
return None
return parsedClassVar
@cache
def ExtractAllClassSigsFromFuncSig(funcSig: str) -> list[str]:
parts = Utils.ExtractTypeTokensFromString(funcSig)
if not len(parts) > 1:
@ -270,6 +344,7 @@ def ExtractAllClassSigsFromFuncSig(funcSig: str) -> list[str]:
listOfClassSigs.append(f"{classType} {className}")
return listOfClassSigs
@cache
def ExtractMainClassSigFromFuncSig(funcSig: str) -> str:
for keyword in ("public:", "protected:", "private:"):
if funcSig.startswith(keyword):
@ -323,24 +398,40 @@ def ExtractMainClassSigFromFuncSig(funcSig: str) -> str:
return f"{'class' if namespacesAndClasses.endswith(funcName) else 'namespace'} {namespacesAndClasses}" if namespacesAndClasses else ""
def BuildParsedClassesLookup(rootClasses: list[ParsedClass], lookupDict: dict[str, ParsedClass]):
lookupDict = {}
def build(parsedClasses: list[ParsedClass]):
for parsedClass in parsedClasses:
lookupDict[parsedClass.fullClassName] = parsedClass
if parsedClass.childClasses:
build(list(parsedClass.childClasses.values()))
build(rootClasses)
def FindParentClassRecursive(fullClassName: str, parsedClassesDict: dict[str, ParsedClass]) -> Optional[ParsedClass]:
if fullClassName in parsedClassesDict:
return parsedClassesDict[fullClassName]
for parsedClass in parsedClassesDict.values():
result = FindParentClassRecursive(fullClassName, parsedClass.childClasses)
if result is not None:
return result
return None
def ParseAllClasses():
global parsedClassesLookupDict
global parsedClassesDict
parsedClassesLookupDict = {}
parsedClassesDict = {}
def MoveChildClasses(parsedClassesDict: dict[str, ParsedClass]):
# Find and move child classes to parent classes
parsedClassesDictCopy = parsedClassesDict.copy()
for childClass in parsedClassesDictCopy.values():
if not childClass.parentNamespaces and not childClass.parentClasses:
continue
parentFullName = childClass.fullClassName
lastClassSeparator = Utils.FindLastClassSeparatorOutsideTemplates(parentFullName)
if lastClassSeparator:
parentFullName = parentFullName[:lastClassSeparator]
parentClass = FindParentClassRecursive(parentFullName, parsedClassesDict)
if not parentClass:
continue
parentClass.childClasses[childClass.fullClassName] = childClass
del parsedClassesDict[childClass.fullClassName]
def ParseAllClasses(parsedClassesDict: dict[str, ParsedClass]):
# Get and parse all classes that are mentioned in a func sig, such as "class cbs::CPointer" in the params here: 'bool cbs::IsInDynamicRoot(class cbs::CPointer<class cbs::CEntity>, bool)'
demangledExportedSigs = IDAUtils.GetDemangledExportedSigs()
for demangledFuncSig in demangledExportedSigs:
global unparsedDemangledExportedSigs
unparsedDemangledExportedSigs = IDAUtils.GetDemangledExportedSigs(Config.INPUT_MD5)
for demangledFuncSig in unparsedDemangledExportedSigs:
listOfExtractedClassSigs = ExtractAllClassSigsFromFuncSig(demangledFuncSig)
for clsSig in listOfExtractedClassSigs:
parsedClass = ParseClassStr(clsSig)
@ -354,7 +445,7 @@ def ParseAllClasses():
alreadyParsedClass.templateParams.extend(parsedClass.templateParams)
# Get and parse the main class that is mentioned in a func sig, such as "cbs" from "cbs::IsInDynamicRoot" in the name of the function here: 'bool cbs::IsInDynamicRoot(class cbs::CPointer<class cbs::CEntity>, bool)'
for demangledFuncSig in demangledExportedSigs:
for demangledFuncSig in unparsedDemangledExportedSigs:
extractedMainClassSig = ExtractMainClassSigFromFuncSig(demangledFuncSig)
parsedClass = ParseClassStr(extractedMainClassSig)
if not parsedClass:
@ -399,34 +490,6 @@ def ParseAllClasses():
if (parentClasses or parsedClass.templateParams) and parsedClass.type == "namespace":
parsedClass.type = "class"
# Find and move child classes to parent classes
parsedClassesCopy = list(parsedClassesDict.values())
for parsedClass in parsedClassesCopy:
if not parsedClass.parentNamespaces and not parsedClass.parentClasses:
continue
parentName = ""
if parsedClass.parentClasses:
parentName = parsedClass.parentClasses[-1]
elif parsedClass.parentNamespaces:
parentName = parsedClass.parentNamespaces[-1]
if not parentName:
continue
parentClass = None
if parsedClass.parentClasses:
parentClass = next((parentClass for parentClass in parsedClassesCopy if parentClass.name == parentName and parentClass.parentClasses == parsedClass.parentClasses[:-1]), None)
elif parsedClass.parentNamespaces:
parentClass = next((parentClass for parentClass in parsedClassesCopy if parentClass.name == parentName and parentClass.parentNamespaces == parsedClass.parentNamespaces[:-1]), None)
if not parentClass:
continue
parentClass.childClasses[parsedClass.fullClassName] = parsedClass
del parsedClassesDict[parsedClass.fullClassName]
# Build the lookup for parsed classes, so we can have faster and more efficient lookup times
BuildParsedClassesLookup(list(parsedClassesDict.values()), parsedClassesLookupDict)
def CreateParamNamesForVTFunc(parsedFunc: ParsedFunction, skipFirstParam: bool) -> str:
paramsList: list[str] = [param.name for param in parsedFunc.params if param.name]
if len(paramsList) == 1 and paramsList[0] == "void":
@ -441,14 +504,9 @@ def CreateParamNamesForVTFunc(parsedFunc: ParsedFunction, skipFirstParam: bool)
return newParams
def ParseClassVTFuncs(parsedClass: ParsedClass):
# Parse child classes first
for parsedChildClass in parsedClass.childClasses.values():
ParseClassVTFuncs(parsedChildClass)
# Parse root class
for (demangledFuncSig, rawType) in RTTIAnalyzer.GetDemangledVTableFuncSigs(parsedClass):
for (demangledFuncSig, rawType) in RTTIAnalyzer.GetDemangledVTableFuncSigs(Config.INPUT_MD5, tuple(parsedClass.parentNamespaces + parsedClass.parentClasses), parsedClass.name):
if rawType:
parsedFunc = ParseFuncStr(rawType, parsedClass, True)
parsedFunc = ParseFuncStr(rawType, parsedClass.fullClassName, True)
if not parsedFunc:
continue
@ -458,7 +516,7 @@ def ParseClassVTFuncs(parsedClass: ParsedClass):
returnTypesStr = ' '.join(returnTypes)
demangledFuncSig = f"{'DUPLICATE_FUNC ' if demangledFuncSig.startswith('DUPLICATE_FUNC') else ''}IDA_GEN_PARSED virtual {returnTypesStr} {demangledFuncSig.removeprefix('DUPLICATE_FUNC').strip()}({newParamTypes})"
elif demangledFuncSig.startswith("DUPLICATE_FUNC"):
parsedFunc = ParseFuncStr(demangledFuncSig.removeprefix("DUPLICATE_FUNC").strip(), parsedClass, True)
parsedFunc = ParseFuncStr(demangledFuncSig.removeprefix("DUPLICATE_FUNC").strip(), parsedClass.fullClassName, True)
if not parsedFunc:
continue
@ -468,34 +526,90 @@ def ParseClassVTFuncs(parsedClass: ParsedClass):
returnTypesStr = ' '.join(returnTypes)
demangledFuncSig = f"DUPLICATE_FUNC {returnTypesStr} {parsedFunc.funcName}({newParamTypes})"
parsedFunc = ParseFuncStr(demangledFuncSig, parsedClass, True)
parsedFunc = ParseFuncStr(demangledFuncSig, parsedClass.fullClassName, True)
if not parsedFunc:
continue
if parsedClass.type == "namespace":
parsedClass.type = "class"
parsedClass.functions.append(parsedFunc)
def ParseAllClassVTFuncs():
# Add dependency classes by going through the func class name, return types and params
if parsedFunc.fullClassName and parsedFunc.fullClassName not in parsedClass.classDependencies:
parsedClass.classDependencies.append(parsedFunc.fullClassName)
for param in parsedFunc.params:
if param.parsedClassParam and param.parsedClassParam.fullClassName and param.parsedClassParam.fullClassName not in parsedClass.classDependencies:
parsedClass.classDependencies.append(param.parsedClassParam.fullClassName)
for returnType in parsedFunc.returnTypes:
if returnType.parsedClassParam and returnType.parsedClassParam.fullClassName and returnType.parsedClassParam.fullClassName not in parsedClass.classDependencies:
parsedClass.classDependencies.append(returnType.parsedClassParam.fullClassName)
# Append parsed func to class
parsedClass.virtualFunctions.append(parsedFunc)
def ParseAllClassVTFuncs(parsedClassesDict: dict[str, ParsedClass]):
for parsedClass in parsedClassesDict.values():
ParseClassVTFuncs(parsedClass)
def ParseAllClassFuncs():
global parsedClassesLookupDict
for demangledExportedSig in IDAUtils.GetDemangledExportedSigs():
parsedFunc = ParseFuncStr(demangledExportedSig, None, False)
def ParseAllClassFuncs(parsedClassesDict: dict[str, ParsedClass]):
global unparsedDemangledExportedSigs
for demangledExportedSig in unparsedDemangledExportedSigs.copy():
parsedFunc = ParseFuncStr(demangledExportedSig, "", False)
if not parsedFunc or not parsedFunc.fullClassName:
continue
parsedClass = parsedClassesLookupDict.get(parsedFunc.fullClassName)
parsedClass = parsedClassesDict.get(parsedFunc.fullClassName)
if not parsedClass:
continue
# Add dependency classes by going through the return types and params
for param in parsedFunc.params:
if param.parsedClassParam and param.parsedClassParam.fullClassName and param.parsedClassParam.fullClassName not in parsedClass.classDependencies:
parsedClass.classDependencies.append(param.parsedClassParam.fullClassName)
for returnType in parsedFunc.returnTypes:
if returnType.parsedClassParam and returnType.parsedClassParam.fullClassName and returnType.parsedClassParam.fullClassName not in parsedClass.classDependencies:
parsedClass.classDependencies.append(returnType.parsedClassParam.fullClassName)
# Append parsed func to class
parsedClass.functions.append(parsedFunc)
unparsedDemangledExportedSigs.remove(demangledExportedSig)
def ParseAllClassVars(parsedClassesDict: dict[str, ParsedClass]):
global unparsedDemangledExportedSigs
for demangledExportedSig in unparsedDemangledExportedSigs.copy():
parsedClassVar = ParseClassVarStr(demangledExportedSig)
if not parsedClassVar or not parsedClassVar.fullClassName:
continue
parsedClass = parsedClassesDict.get(parsedClassVar.fullClassName)
if not parsedClass:
continue
parsedClass.functions.append(parsedFunc)
# Add dependency classes by going through the var types
for varType in parsedClassVar.varTypes:
if varType.parsedClassParam and varType.parsedClassParam.fullClassName and varType.parsedClassParam.fullClassName not in parsedClass.classDependencies:
parsedClass.classDependencies.append(varType.parsedClassParam.fullClassName)
def GetAllParsedClasses():
ParseAllClasses()
ParseAllClassVTFuncs()
ParseAllClassFuncs()
with open(Config.PARSED_CLASSES_OUTPUT_FILE, 'w') as fileStream:
json.dump(parsedClassesDict, fileStream, indent=4)
# Append parsed func to class
parsedClass.classVars.append(parsedClassVar)
unparsedDemangledExportedSigs.remove(demangledExportedSig)
def GetAllParsedClasses() -> dict[str, ParsedClass]:
global parsedClassesDict
if os.path.exists(Config.PARSED_CLASSES_OUTPUT_FILE) and not parsedClassesDict:
with open(Config.PARSED_CLASSES_OUTPUT_FILE, 'r') as fileStream:
data = json.load(fileStream)
parsedClassesDict = { key: ParsedClass.model_validate(value) for key, value in data.items() }
elif not parsedClassesDict:
parsedClassesDict.clear()
ParseAllClasses(parsedClassesDict)
ParseAllClassVTFuncs(parsedClassesDict)
ParseAllClassFuncs(parsedClassesDict)
ParseAllClassVars(parsedClassesDict)
#MoveChildClasses(parsedClassesDict)
with open(Config.PARSED_CLASSES_OUTPUT_FILE, 'w') as fileStream:
fileStream.write(json.dumps(parsedClassesDict, indent=4, default=DefaultPydanticSerializer))
return parsedClassesDict

View File

@ -7,14 +7,13 @@ def Main():
UI.OpenMainDlg()
# Reload modules to apply any changes
from ExportClassH import Config, Utils, IDAUtils, ClassDefs, JSONGen, RTTIAnalyzer#, ClassParser, HeaderGen, ProjectManager
from ExportClassH import Config, Utils, IDAUtils, ClassDefs, JSONGen, RTTIAnalyzer, HeaderGen#, ProjectManager
importlib.reload(Config)
importlib.reload(Utils)
importlib.reload(IDAUtils)
importlib.reload(ClassDefs)
importlib.reload(JSONGen)
importlib.reload(RTTIAnalyzer)
# importlib.reload(ClassParser)
# importlib.reload(HeaderGen)
importlib.reload(HeaderGen)
# importlib.reload(ProjectManager)
importlib.reload(UI)

View File

@ -1,14 +1,13 @@
import struct
import idc
import idaapi
import ida_bytes
import ida_ida
import idautils
import ida_hexrays
from functools import cache
from ExportClassH import Utils
from ExportClassH.ClassDefs import ParsedClass
from ExportClassH import IDAUtils, Config
def GetVTablePtr(targetClass: ParsedClass, targetClassRTTIName: str = "") -> int:
@cache
def GetVTablePtr(inputMD5: bytes, parentNamespacesClasses: tuple[str, ...], targetClassName: str, targetClassRTTIName: str = "") -> int:
"""
Find vtable pointer for a class using RTTI information.
Supports both simple class names and namespaced class names.
@ -16,86 +15,55 @@ def GetVTablePtr(targetClass: ParsedClass, targetClassRTTIName: str = "") -> int
Returns the vtable pointer (an integer) or 0 if not found.
"""
baseDLLAddr: int = idaapi.get_imagebase()
# Use provided RTTI name if available (for templates), otherwise generate it
if not targetClassRTTIName:
# Check if this is a templated class
typeDescriptorName: str = Utils.GetMangledTypePrefix(tuple(targetClass.parentNamespaces + targetClass.parentClasses), targetClass.name)
typeDescriptorName: str = IDAUtils.GetMangledTypePrefix(parentNamespacesClasses, targetClassName)
else:
# Use the provided RTTI name directly
typeDescriptorName: str = targetClassRTTIName
# Search for the RTTI type descriptor
typeDescriptorBytes: bytes = typeDescriptorName.encode('ascii')
idaPattern: str = Utils.BytesToIDAPattern(typeDescriptorBytes)
# Search in .rdata
rdataStartAddr, rdataSize = Utils.GetSectionInfo(".rdata")
if not rdataStartAddr:
rttiStringsList = IDAUtils.GetIDARTTIStringsList(Config.INPUT_MD5)
if not rttiStringsList:
return 0
# Look for the type descriptor
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, idaPattern, 16, Utils.IDA_NALT_ENCODING)
if errorParsingIDAPattern:
return 0
typeDescriptorPatternAddr: int = ida_bytes.bin_search(rdataStartAddr, ida_ida.cvar.inf.max_ea, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
if typeDescriptorPatternAddr == idc.BADADDR:
typeDescriptorAddr = rttiStringsList.get(typeDescriptorName)
if not typeDescriptorAddr or typeDescriptorAddr == idc.BADADDR:
return 0
# Adjust to get RTTI type descriptor
rttiTypeDescriptorAddr: int = typeDescriptorPatternAddr - 0x10
rttiTypeDescriptorAddr: int = typeDescriptorAddr - 0x10
xrefsToRTTITypeDescriptor = idautils.DataRefsTo(rttiTypeDescriptorAddr)
if not xrefsToRTTITypeDescriptor:
return 0
# Compute offset relative to base address
rttiTypeDescriptorOffset: int = rttiTypeDescriptorAddr - baseDLLAddr
rttiTypeDescriptorOffsetBytes: bytes = struct.pack("<I", rttiTypeDescriptorOffset)
rttiTypeDescriptorOffsetPattern: str = Utils.BytesToIDAPattern(rttiTypeDescriptorOffsetBytes)
# Search for references to this offset
xrefs: list[int] = Utils.FindAllPatternsInRange(rttiTypeDescriptorOffsetPattern, rdataStartAddr, rdataSize)
# Analyze each reference to find the vtable
for xref in xrefs:
xref: int
for xrefToRTTITypeDescriptor in xrefsToRTTITypeDescriptor:
# Check offset from class
offsetFromClass: int = idc.get_wide_dword(xref - 8)
offsetFromClass: int = idc.get_wide_dword(xrefToRTTITypeDescriptor - 0x8)
if offsetFromClass:
continue
# Get object locator
objectLocatorOffsetAddr: int = xref - 0xC
# Look for references to the object locator
objectLocatorBytes: bytes = struct.pack("<Q", objectLocatorOffsetAddr)
objectLocatorPattern: str = Utils.BytesToIDAPattern(objectLocatorBytes)
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, objectLocatorPattern, 16, Utils.IDA_NALT_ENCODING)
if errorParsingIDAPattern:
continue
objectLocatorAddr: int = ida_bytes.bin_search(rdataStartAddr, ida_ida.cvar.inf.max_ea, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
if objectLocatorAddr == idc.BADADDR:
continue
# Vtable pointer is at (objectLocatorAddr + 0x8)
vtableAddr: int = objectLocatorAddr + 8
if vtableAddr <= 8:
continue
return vtableAddr
objectLocatorAddr: int = xrefToRTTITypeDescriptor - 0xC
xrefsToObjectLocator = idautils.DataRefsTo(objectLocatorAddr)
if not xrefsToObjectLocator:
return 0
for xrefToObjectLocator in xrefsToObjectLocator:
# Vtable pointer is at (objectLocatorAddr + 0x8)
vtableAddr: int = xrefToObjectLocator + 0x8
if vtableAddr <= 0x8:
break
return vtableAddr
return 0
def GetDemangledVTableFuncSigs(targetClass: ParsedClass, targetClassRTTIName: str = "") -> list[tuple[str, str]]:
@cache
def GetDemangledVTableFuncSigs(inputMD5: bytes, parentNamespacesClasses: tuple[str, ...], targetClassName: str, targetClassRTTIName: str = "") -> list[tuple[str, str]]:
"""
Get the ordered list of function names from a class's vtable.
For templated classes, you can provide the rtti_name pattern.
"""
vtablePtr: int = GetVTablePtr(targetClass, targetClassRTTIName)
vtablePtr: int = GetVTablePtr(inputMD5, parentNamespacesClasses, targetClassName, targetClassRTTIName)
if not vtablePtr:
return []
@ -112,7 +80,7 @@ def GetDemangledVTableFuncSigs(targetClass: ParsedClass, targetClassRTTIName: st
break
funcSig: str = idc.get_func_name(ptr)
demangledFuncSig: str = Utils.DemangleSig(funcSig)
demangledFuncSig: str = IDAUtils.DemangleSig(funcSig)
demangledFuncSig = demangledFuncSig if demangledFuncSig else funcSig
rawType: str = ""

View File

@ -1,8 +1,10 @@
import os
import json
import ida_kernwin
import idc
import cProfile
from ExportClassH import Config, JSONGen
from ExportClassH import Config, HeaderGen
def SetConfigVars(settings):
Config.PROJECT_INCLUDES_PATH = settings["PROJECT_INCLUDES_PATH"]
@ -10,12 +12,12 @@ def SetConfigVars(settings):
Config.LAST_CLICKED_RADIO = settings["LAST_CLICKED_RADIO"]
Config.HEADER_OUTPUT_PATH = os.path.join(Config.OUTPUT_PATH, "generated")
Config.CACHE_OUTPUT_PATH = os.path.join(Config.OUTPUT_PATH, "cache")
Config.PARSED_VARS_CACHE_FILENAME = os.path.join(Config.CACHE_OUTPUT_PATH, "parsedClassVarsByClass.cache")
Config.PARSED_FUNCS_CACHE_FILENAME = os.path.join(Config.CACHE_OUTPUT_PATH, "parsedFuncsByClass.cache")
# Load settings from file
def LoadConfig():
def LoadConfig() -> dict:
Config.INPUT_MD5 = idc.retrieve_input_file_md5()
if not Config.INPUT_MD5:
raise Exception("Failed retreiving input file MD5")
if os.path.exists(Config.CONFIG_FILE):
with open(Config.CONFIG_FILE, "r") as f:
loadedJson = json.load(f)
@ -73,11 +75,12 @@ Export Class to C++ Header
{FormChangeCb}
<##Update Project Code:{r_update}>
<##Generate Class Code:{r_generate}>
<##Update Project:{r_update}>
<##Generate Classes:{r_classesGenerate}>
<##Generate Class:{r_classGenerate}>
<##Settings:{r_settings}>{radioGroup}>
""", {
'radioGroup': ida_kernwin.Form.RadGroupControl(("r_update", "r_generate", "r_settings")),
'radioGroup': ida_kernwin.Form.RadGroupControl(("r_update", "r_classesGenerate", "r_classGenerate", "r_settings")),
'FormChangeCb': ida_kernwin.Form.FormChangeCb(self.OnFormChange),
})
@ -116,17 +119,19 @@ def OpenMainDlg():
SaveConfig()
if selectedOption == 0:
print("[INFO] Update Project Code selected!")
print("[INFO] Update Project selected!")
#ProjectManager.ProcessExistingHeaders()
elif selectedOption == 1:
print("[INFO] Generate Class Code selected!")
# targetClassName = ida_kernwin.ask_str("", 0, "Enter target class name:")
# if not targetClassName:
# print("No target class specified. Aborting.")
# return
#HeaderGen.ExportClassHeader(ClassName(targetClassName))
JSONGen.GetAllParsedClasses()
print("[INFO] Generate Classes selected!")
cProfile.runctx('HeaderGen.ExportClassHeaders()', globals(), locals(), 'cProfiler-data.dat')
elif selectedOption == 2:
print("[INFO] Generate Class selected!")
targetClass = ida_kernwin.ask_str("", 0, "Enter target class name:")
if not targetClass:
print("No target class specified. Aborting.")
return
cProfile.runctx('HeaderGen.ExportClassHeader(targetClass)', globals(), locals(), 'cProfiler-data.dat')
elif selectedOption == 3:
print("[INFO] Settings selected!")
OpenSettingsDlg() # Open settings when selected
else:

View File

@ -1,25 +1,24 @@
import re
from functools import cache
from typing import Tuple
import ida_nalt
import ida_bytes
import idaapi
import idautils
import idc
IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B)
_RE_BEFORE_POINTER = re.compile(r'\s+([*&])')
_RE_AFTER_POINTER = re.compile(r'([*&])(?![\s*&])')
_RE_COMMA = re.compile(r'\s*,\s*')
_RE_AFTER_ANGLE = re.compile(r'<\s+')
_RE_BEFORE_ANGLE = re.compile(r'\s+>')
_RE_BEFORE_PUNC = re.compile(r'\s+([\),])')
_RE_SPACES = re.compile(r'\s+')
@cache
def FixTypeSpacing(type: str) -> str:
"""Fix spacing for pointers/references, commas, and angle brackets."""
type = re.sub(r'\s+([*&])', r'\1', type) # Remove space before '*' or '&'
type = re.sub(r'([*&])(?![\s*&])', r'\1 ', type) # Ensure '*' or '&' is followed by one space if it's not already.
type = re.sub(r'\s*,\s*', ', ', type) # Ensure comma followed by one space
type = re.sub(r'<\s+', '<', type) # Remove space after '<'
type = re.sub(r'\s+>', '>', type) # Remove space before '>'
type = re.sub(r'\s+([\),])', r'\1', type)
type = re.sub(r'\s+', ' ', type) # Collapse multiple spaces
return type.strip()
def FixTypeSpacing(type_str: str) -> str:
type_str = _RE_BEFORE_POINTER.sub(r'\1', type_str)
type_str = _RE_AFTER_POINTER.sub(r'\1 ', type_str)
type_str = _RE_COMMA.sub(', ', type_str)
type_str = _RE_AFTER_ANGLE.sub('<', type_str)
type_str = _RE_BEFORE_ANGLE.sub('>', type_str)
type_str = _RE_BEFORE_PUNC.sub(r'\1', type_str)
type_str = _RE_SPACES.sub(' ', type_str)
return type_str.strip()
@cache
def CleanDoubleSpaces(str: str) -> str:
@ -78,6 +77,16 @@ def ExtractTypeTokensFromString(types: str) -> list[str]:
# Filter out empty strings
return [word.strip() for word in result if word]
@cache
def ExtractParamNames(params: str) -> str:
paramsList: list[str] = [param.strip() for param in params.split(',') if param.strip()]
if len(paramsList) == 1 and paramsList[0] == "void":
return ""
paramNames: list[str] = [param.split(" ")[-1].strip() for param in paramsList]
newParams: str = ", ".join(paramNames)
return newParams
@cache
def SplitByCommaOutsideTemplates(params: str) -> list[str]:
parts = []
@ -165,67 +174,4 @@ def FindLastClassSeparatorOutsideTemplates(s: str) -> int:
# Only if we're not inside a template.
if depth == 0 and i > 0 and s[i-1:i+1] == "::":
return i - 1 # return the index of the first colon
return -1
# -----------------------------------------------------------------------------
# IDA util functions
# -----------------------------------------------------------------------------
@cache
def DemangleSig(sig: str) -> str:
return idaapi.demangle_name(sig, idaapi.MNG_LONG_FORM)
@cache
def GetMangledTypePrefix(namespaces: tuple[str, ...], className: str) -> str:
"""
Get the appropriate mangled type prefix for a class name.
For class "X" this would be ".?AVX@@"
For class "NS::X" this would be ".?AVX@NS@@"
For templated classes, best to use get_mangled_name_for_template instead.
"""
if not namespaces:
return f".?AV{className}@@"
# For namespaced classes, the format is .?AVClassName@Namespace@@
# For nested namespaces, they are separated with @ in reverse order
mangledNamespaces = "@".join(reversed(namespaces))
return f".?AV{className}@{mangledNamespaces}@@"
# -----------------------------------------------------------------------------
# IDA pattern search utilities
# -----------------------------------------------------------------------------
@cache
def BytesToIDAPattern(data: bytes) -> str:
"""Convert bytes to IDA-friendly hex pattern string."""
return " ".join("{:02X}".format(b) for b in data)
def GetSectionInfo(sectionName: str) -> Tuple[int, int]:
"""Get start address and size of a specified section."""
for seg_ea in idautils.Segments():
if idc.get_segm_name(seg_ea) == sectionName:
start = seg_ea
end = idc.get_segm_end(seg_ea)
return start, end - start
return 0, 0
def FindAllPatternsInRange(pattern: str, start: int, size: int) -> list[int]:
"""Find all occurrences of a pattern within a memory range."""
addresses: list[int] = []
ea: int = start
end: int = start + size
while ea < end:
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, pattern, 16, IDA_NALT_ENCODING)
if errorParsingIDAPattern:
return []
patternAddr: int = ida_bytes.bin_search(ea, end, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
if patternAddr == idc.BADADDR:
break
addresses.append(patternAddr)
ea = patternAddr + 8 # advance past found pattern
return addresses
return -1

Binary file not shown.

11156
_IDAScripts/generated/cbs.h Normal file

File diff suppressed because it is too large Load Diff