Struct reading and disassembly script overhaul, various misc. loading fixes, bump to .NET 9 (#13)

* Bump projects to .net 9 and update nugets

* add VersionedSerialization + source generator

* migrate versioning to StructVersion class, add handling/detection for 29.2/31.2

* add new struct definitions

* rename serialization methods and add BinaryObjectStreamReader for interop

* Rework metadata struct loading to use new struct versioning

* move 29/31.1/.2 to use tags (-2022,-2023) instead of minor versions

* fix metadata usage validity checks

* rework code registration offsetting a bit and add second 29/31.1 condition

* tweak .1 condition (again)

* 29/31.2 was a psyop

* also remove 29.2 from the readme

* remove loading of packed dlls - this was a very unsafe feature

* support auto-recovering type indices from type handles
fixes loading of memory-dumped v29+ libraries since those replacee their class indices on load with a pointer to the corresponding type

* support loading PEs without an export table

* also read UnresolvedVirtualCallCount on regular v31

* Disable plugin loading for now

* Overhaul disassembler script + add Binary Ninja target (#12)

* Overhaul diassembler scripts:
- No longer defines top level functions
- Split into three classes: StatusHandler (like before), DisassemblerInterface (for interfacing with the used program API), ScriptContext (for definiting general functions that use the disassembler interface)
- Add type annotations to all class methods and remove 2.7 compatibility stuff (Ghidra now supports Python 3 so this is unnecessary anymore)
- Disassembler backends are now responsible for launching metadata/script processing, to better support disassembler differences
- String handling is back in the base ScriptContext class, disassembler interfaces opt into the fake string segment creation and fall back to the old method if it isn't supported

* Add Binary Ninja disassembler script backend
This uses the new backend-controlled execution to launch metadata processing on a background thread to keep the ui responsive

* make binary ninja script use own _BINARYNINJA_ define and add define helpers to header

* Update README to account for new script and binary ninja backend

* implement fake string segment functions for binary ninja but don't advertise support

* also cache API function types in binary ninja backend

* fix ida script and disable folders again

* Fix metadata usage issues caused by it being a value type now

* make TryMapVATR overrideable and implement it for ELFs

* Make field offset reading use TryMapVATR to reduce exceptions

* Fix NRE in Assembly ctor on < v24.2

* Update actions workflow to produce cross-platform CLI binaries, update readme to reflect .net 9 changes

* workflow: only restore packages for projects that are being built

* workflow: tweak caching and fix gui compilation

* workflow: remove double .zip in CLI artifact name

* 29/31.2 don't actually exist, this logic is not needed
This commit is contained in:
Luke
2024-11-14 14:32:11 +01:00
committed by GitHub
parent 5b0476fcc5
commit b05c03964a
130 changed files with 5117 additions and 4371 deletions

View File

@@ -12,6 +12,7 @@ using System.IO;
using System.Linq;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using Il2CppInspector.Next;
using Il2CppInspector.Reflection;
namespace Il2CppInspector.Outputs
@@ -186,7 +187,7 @@ namespace Il2CppInspector.Outputs
};
if (mType.IsExplicitLayout || mType.IsSequentialLayout)
mType.ClassLayout = new ClassLayoutUser(1, (uint)type.Sizes.nativeSize);
mType.ClassLayout = new ClassLayoutUser(1, (uint)type.Sizes.NativeSize);
// Add nested types
foreach (var nestedType in type.DeclaredNestedTypes)
@@ -241,7 +242,7 @@ namespace Il2CppInspector.Outputs
AddMethod(module, mType, method);
// Add token attribute
if (type.Definition != null)
if (type.Definition.IsValid)
mType.AddAttribute(module, tokenAttribute, ("Token", $"0x{type.MetadataToken:X8}"));
// Add custom attribute attributes
@@ -269,7 +270,7 @@ namespace Il2CppInspector.Outputs
if (field.HasFieldRVA) {
// Attempt to get field size
var fieldSize = field.FieldType.Sizes.nativeSize;
var fieldSize = field.FieldType.Sizes.NativeSize;
var preview = model.Package.Metadata.ReadBytes((long) field.DefaultValueMetadataAddress, fieldSize);
mField.InitialValue = preview;
@@ -310,7 +311,7 @@ namespace Il2CppInspector.Outputs
// Add token attribute
// Generic properties and constructed properties (from disperate get/set methods) have no definition
if (prop.Definition != null)
if (prop.Definition.IsValid)
mProp.AddAttribute(module, tokenAttribute, ("Token", $"0x{prop.MetadataToken:X8}"));
// Add custom attribute attributes
@@ -432,8 +433,8 @@ namespace Il2CppInspector.Outputs
("Offset", string.Format("0x{0:X}", model.Package.BinaryImage.MapVATR(method.VirtualAddress.Value.Start))),
("VA", method.VirtualAddress.Value.Start.ToAddressString())
};
if (method.Definition.slot != ushort.MaxValue)
args.Add(("Slot", method.Definition.slot.ToString()));
if (method.Definition.Slot != ushort.MaxValue)
args.Add(("Slot", method.Definition.Slot.ToString()));
mMethod.AddAttribute(module, addressAttribute, args.ToArray());
}
@@ -591,7 +592,7 @@ namespace Il2CppInspector.Outputs
// Create folder for DLLs
Directory.CreateDirectory(outputPath);
if (model.Package.Version >= 29)
if (model.Package.Version >= MetadataVersions.V290)
{
// We can now apply all attributes directly.
directApplyAttributes = model.TypesByDefinitionIndex
@@ -648,7 +649,7 @@ namespace Il2CppInspector.Outputs
AddCustomAttribute(module, module.Assembly, ca);
// Add token attributes
module.AddAttribute(module, tokenAttribute, ("Token", $"0x{asm.ImageDefinition.token:X8}"));
module.AddAttribute(module, tokenAttribute, ("Token", $"0x{asm.ImageDefinition.Token:X8}"));
module.Assembly.AddAttribute(module, tokenAttribute, ("Token", $"0x{asm.MetadataToken:X8}"));
if (types.TryGetValue(module, out var shallowTypes))

View File

@@ -353,7 +353,7 @@ namespace Il2CppInspector.Outputs
foreach (var asm in assemblies) {
text.Append($"// Image {asm.Index}: {asm.ShortName} - Assembly: {asm.FullName}");
if (!SuppressMetadata)
text.Append($" - Types {asm.ImageDefinition.typeStart}-{asm.ImageDefinition.typeStart + asm.ImageDefinition.typeCount - 1}");
text.Append($" - Types {asm.ImageDefinition.TypeStart}-{asm.ImageDefinition.TypeStart + asm.ImageDefinition.TypeCount - 1}");
text.AppendLine();
// Assembly-level attributes
@@ -426,7 +426,7 @@ namespace Il2CppInspector.Outputs
sb.Append($" // Metadata: {field.DefaultValueMetadataAddress.ToAddressString()}");
// For static array initializers, output metadata address and preview
if (field.HasFieldRVA && !SuppressMetadata) {
var preview = model.Package.Metadata.ReadBytes((long) field.DefaultValueMetadataAddress, field.FieldType.Sizes.nativeSize);
var preview = model.Package.Metadata.ReadBytes((long) field.DefaultValueMetadataAddress, field.FieldType.Sizes.NativeSize);
sb.Append($" // Static value (base64): {Convert.ToBase64String(preview)} - Metadata: {field.DefaultValueMetadataAddress.ToAddressString()}");
}
sb.Append("\n");

View File

@@ -38,14 +38,15 @@ namespace Il2CppInspector.Outputs
using var fs = new FileStream(typeHeaderFile, FileMode.Create);
_writer = new StreamWriter(fs, Encoding.ASCII);
const string decompilerIfDef = "#if !defined(_GHIDRA_) && !defined(_IDA_) && !defined(_IDACLANG_)";
using (_writer)
{
writeHeader();
// Write primitive type definitions for when we're not including other headers
writeCode($"""
#define IS_LIBCLANG_DECOMPILER (defined(_IDACLANG_) || defined(_BINARYNINJA_))
#define IS_DECOMPILER (defined(_GHIDRA_) || defined(_IDA_) || IS_LIBCLANG_DECOMPILER)
#if defined(_GHIDRA_) || defined(_IDA_)
typedef unsigned __int8 uint8_t;
typedef unsigned __int16 uint16_t;
@@ -57,7 +58,7 @@ namespace Il2CppInspector.Outputs
typedef __int64 int64_t;
#endif
#ifdef _IDACLANG_
#if IS_LIBCLANG_DECOMPILER
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
@@ -68,13 +69,13 @@ namespace Il2CppInspector.Outputs
typedef long int64_t;
#endif
#if defined(_GHIDRA_) || defined(_IDACLANG_)
#if defined(_GHIDRA_) || IS_LIBCLANG_DECOMPILER
typedef int{_model.Package.BinaryImage.Bits}_t intptr_t;
typedef uint{_model.Package.BinaryImage.Bits}_t uintptr_t;
typedef uint{_model.Package.BinaryImage.Bits}_t size_t;
#endif
{decompilerIfDef}
#if !IS_DECOMPILER
#define _CPLUSPLUS_
#endif
""");
@@ -114,7 +115,7 @@ namespace Il2CppInspector.Outputs
}
// C does not support namespaces
writeCode($"{decompilerIfDef}");
writeCode("#if !IS_DECOMPILER");
writeCode("namespace app {");
writeCode("#endif");
writeLine("");
@@ -124,7 +125,7 @@ namespace Il2CppInspector.Outputs
writeTypesForGroup("Application types from usages", "types_from_usages");
writeTypesForGroup("Application unused value types", "unused_concrete_types");
writeCode($"{decompilerIfDef}");
writeCode("#if !IS_DECOMPILER");
writeCode("}");
writeCode("#endif");
}
@@ -248,7 +249,7 @@ namespace Il2CppInspector.Outputs
using (_writer)
{
writeHeader();
writeCode($"#define __IL2CPP_METADATA_VERSION {_model.Package.Version * 10:F0}");
writeCode($"#define __IL2CPP_METADATA_VERSION {_model.Package.Version.Major * 10 + _model.Package.Version.Minor * 10:F0}");
}
// Write boilerplate code

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Text.Json;
using Il2CppInspector.Reflection;
using Il2CppInspector.Model;
using Il2CppInspector.Next;
namespace Il2CppInspector.Outputs
{
@@ -182,8 +183,8 @@ namespace Il2CppInspector.Outputs
// TODO: In the future, add data ranges for the entire IL2CPP metadata tree
writeArray("arrayMetadata", () => {
if (model.Package.Version >= 24.2) {
writeObject(() => writeTypedArray(binary.CodeRegistration.pcodeGenModules, binary.Modules.Count, "struct Il2CppCodeGenModule *", "g_CodeGenModules"));
if (model.Package.Version >= MetadataVersions.V242) {
writeObject(() => writeTypedArray(binary.CodeRegistration.CodeGenModules, binary.Modules.Count, "struct Il2CppCodeGenModule *", "g_CodeGenModules"));
}
}, "IL2CPP Array Metadata");
}

View File

@@ -22,7 +22,9 @@ namespace Il2CppInspector.Outputs
public static IEnumerable<string> GetAvailableTargets() {
var ns = typeof(PythonScript).Namespace + ".ScriptResources.Targets";
var res = ResourceHelper.GetNamesForNamespace(ns);
return res.Select(s => Path.GetFileNameWithoutExtension(s.Substring(ns.Length + 1))).OrderBy(s => s);
return res
.Select(s => Path.GetFileNameWithoutExtension(s[(ns.Length + 1)..]))
.OrderBy(s => s);
}
// Output script file
@@ -52,12 +54,11 @@ namespace Il2CppInspector.Outputs
var jsonMetadataRelativePath = getRelativePath(outputFile, jsonMetadataFile);
var ns = typeof(PythonScript).Namespace + ".ScriptResources";
var preamble = ResourceHelper.GetText(ns + ".shared-preamble.py");
var main = ResourceHelper.GetText(ns + ".shared-main.py");
var api = ResourceHelper.GetText($"{ns}.Targets.{target}.py");
var ns = $"{typeof(PythonScript).Namespace}.ScriptResources";
var baseScipt = ResourceHelper.GetText($"{ns}.shared_base.py");
var impl = ResourceHelper.GetText($"{ns}.Targets.{target}.py");
var script = string.Join("\n", new [] { preamble, api, main })
var script = string.Join("\n", baseScipt, impl)
.Replace("%SCRIPTFILENAME%", Path.GetFileName(outputFile))
.Replace("%TYPE_HEADER_RELATIVE_PATH%", typeHeaderRelativePath.ToEscapedString())
.Replace("%JSON_METADATA_RELATIVE_PATH%", jsonMetadataRelativePath.ToEscapedString())

View File

@@ -0,0 +1,284 @@
from binaryninja import *
#try:
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# from ..shared_base import BaseStatusHandler, BaseDisassemblerInterface, ScriptContext
# import json
# import os
# import sys
# from datetime import datetime
#except:
# pass
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
class BinaryNinjaDisassemblerInterface(BaseDisassemblerInterface):
# this is implemented,
# however the write API does not seem to work properly here (possibly a bug),
# so this is disabled for now
supports_fake_string_segment: bool = False
_status: BaseStatusHandler
_view: BinaryView
_undo_id: str
_components: dict[str, Component]
_type_cache: dict[str, Type]
_function_type_cache: dict[str, Type]
_address_size: int
_endianness: Literal["little", "big"]
def __init__(self, status: BaseStatusHandler):
self._status = status
def _get_or_create_type(self, type: str) -> Type:
if type.startswith("struct "):
type = type[len("struct "):]
elif type.startswith("class "):
type = type[len("class "):]
if type in self._type_cache:
return self._type_cache[type]
if type.endswith("*"):
base_type = self._get_or_create_type(type[:-1].strip())
parsed = PointerType.create(self._view.arch, base_type) # type: ignore
else:
parsed = self._view.get_type_by_name(type)
if parsed is None:
parsed, errors = self._view.parse_type_string(type)
self._type_cache[type] = parsed
return parsed
def get_script_directory(self) -> str:
return CURRENT_PATH
def on_start(self):
self._view = bv # type: ignore
self._undo_id = self._view.begin_undo_actions()
self._view.set_analysis_hold(True)
self._components = {}
self._type_cache = {}
self._function_type_cache = {}
self._address_size = self._view.address_size
self._endianness = "little" if self._view.endianness == Endianness.LittleEndian else "big"
self._status.update_step("Parsing header")
with open(os.path.join(self.get_script_directory(), "il2cpp.h"), "r") as f:
parsed_types, errors = TypeParser.default.parse_types_from_source(
f.read(),
"il2cpp.h",
self._view.platform if self._view.platform is not None else Platform["windows-x86_64"],
self._view,
[
"--target=x86_64-pc-linux",
"-x", "c++",
"-D_BINARYNINJA_=1"
]
)
if parsed_types is None:
log_error("Failed to import header")
log_error(errors)
return
self._status.update_step("Importing header types", len(parsed_types.types))
def import_progress_func(progress: int, total: int):
self._status.update_progress(1)
return True
self._view.define_user_types([(x.name, x.type) for x in parsed_types.types], import_progress_func)
def on_finish(self):
self._view.commit_undo_actions(self._undo_id)
self._view.set_analysis_hold(False)
self._view.update_analysis()
def define_function(self, address: int, end: int | None = None):
if self._view.get_function_at(address) is not None:
return
self._view.create_user_function(address)
def define_data_array(self, address: int, type: str, count: int):
parsed_type = self._get_or_create_type(type)
array_type = ArrayType.create(parsed_type, count)
var = self._view.get_data_var_at(address)
if var is None:
self._view.define_user_data_var(address, array_type)
else:
var.type = array_type
def set_data_type(self, address: int, type: str):
var = self._view.get_data_var_at(address)
dtype = self._get_or_create_type(type)
if var is None:
self._view.define_user_data_var(address, dtype)
else:
var.type = dtype
def set_function_type(self, address: int, type: str):
function = self._view.get_function_at(address)
if function is None:
return
if type in self._function_type_cache:
function.type = self._function_type_cache[type] # type: ignore
else:
#log_info(f"skipping function type setting for {address}, {type}")
#pass
function.type = type.replace("this", "`this`")
def set_data_comment(self, address: int, cmt: str):
self._view.set_comment_at(address, cmt)
def set_function_comment(self, address: int, cmt: str):
function = self._view.get_function_at(address)
if function is None:
return
function.comment = cmt
def set_data_name(self, address: int, name: str):
var = self._view.get_data_var_at(address)
if var is None:
return
if name.startswith("_Z"):
type, demangled = demangle_gnu3(self._view.arch, name, self._view)
var.name = get_qualified_name(demangled)
else:
var.name = name
def set_function_name(self, address: int, name: str):
function = self._view.get_function_at(address)
if function is None:
return
if name.startswith("_Z"):
type, demangled = demangle_gnu3(self._view.arch, name, self._view)
function.name = get_qualified_name(demangled)
#function.type = type - this does not work due to the generated types not being namespaced. :(
else:
function.name = name
def add_cross_reference(self, from_address: int, to_address: int):
self._view.add_user_data_ref(from_address, to_address)
def import_c_typedef(self, type_def: str):
self._view.define_user_type(None, type_def)
# optional
def _get_or_create_component(self, name: str):
if name in self._components:
return self._components[name]
current = name
if current.count("/") != 0:
split_idx = current.rindex("/")
parent, child = current[:split_idx], current[split_idx:]
parent = self._get_or_create_component(name)
component = self._view.create_component(child, parent)
else:
component = self._view.create_component(name)
self._components[name] = component
return component
def add_function_to_group(self, address: int, group: str):
return
function = self._view.get_function_at(address)
if function is None:
return
self._get_or_create_component(group).add_function(function)
def cache_function_types(self, signatures: list[str]):
function_sigs = set(signatures)
if len(function_sigs) == 0:
return
typestr = ";\n".join(function_sigs).replace("this", "_this") + ";"
res = self._view.parse_types_from_string(typestr)
for function_sig, function in zip(function_sigs, res.functions.values()): # type: ignore
self._function_type_cache[function_sig] = function
# only required if supports_fake_string_segment == True
def create_fake_segment(self, name: str, size: int) -> int:
last_end_addr = self._view.mapped_address_ranges[-1].end
if last_end_addr % 0x1000 != 0:
last_end_addr += (0x1000 - (last_end_addr % 0x1000))
self._view.add_user_segment(last_end_addr, size, 0, 0, SegmentFlag.SegmentContainsData)
self._view.add_user_section(name, last_end_addr, size, SectionSemantics.ReadOnlyDataSectionSemantics)
return last_end_addr
def write_string(self, address: int, value: str):
self._view.write(address, value.encode() + b"\x00")
def write_address(self, address: int, value: int):
self._view.write(address, value.to_bytes(self._address_size, self._endianness))
class BinaryNinjaStatusHandler(BaseStatusHandler):
def __init__(self, thread: BackgroundTaskThread):
self.step = "Initializing"
self.max_items = 0
self.current_items = 0
self.start_time = datetime.now()
self.step_start_time = self.start_time
self.last_updated_time = datetime.min
self._thread = thread
def initialize(self): pass
def update(self):
if self.was_cancelled():
raise RuntimeError("Cancelled script.")
current_time = datetime.now()
if 0.5 > (current_time - self.last_updated_time).total_seconds():
return
self.last_updated_time = current_time
step_time = current_time - self.step_start_time
total_time = current_time - self.start_time
self._thread.progress = f"Processing IL2CPP metadata: {self.step} ({self.current_items}/{self.max_items}), elapsed: {step_time} ({total_time})"
def update_step(self, step, max_items = 0):
self.step = step
self.max_items = max_items
self.current_items = 0
self.step_start_time = datetime.now()
self.last_updated_time = datetime.min
self.update()
def update_progress(self, new_progress = 1):
self.current_items += new_progress
self.update()
def was_cancelled(self): return False
def close(self):
pass
# Entry point
class Il2CppTask(BackgroundTaskThread):
def __init__(self):
BackgroundTaskThread.__init__(self, "Processing IL2CPP metadata...", False)
def run(self):
status = BinaryNinjaStatusHandler(self)
backend = BinaryNinjaDisassemblerInterface(status)
context = ScriptContext(backend, status)
context.process()
Il2CppTask().start()

View File

@@ -1,106 +1,124 @@
# Ghidra-specific implementation
from ghidra.app.cmd.function import ApplyFunctionSignatureCmd
from ghidra.app.script import GhidraScriptUtil
from ghidra.app.util.cparser.C import CParserUtils
from ghidra.program.model.data import ArrayDataType
from ghidra.program.model.symbol import SourceType
from ghidra.program.model.symbol import RefType
from ghidra.app.cmd.label import DemanglerCmd
xrefs = currentProgram.getReferenceManager()
#try:
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# from ..shared_base import BaseStatusHandler, BaseDisassemblerInterface, ScriptContext
# import json
# import os
# import sys
# from datetime import datetime
#except:
# pass
def set_name(addr, name):
if not name.startswith("_ZN"):
createLabel(toAddr(addr), name, True)
return
cmd = DemanglerCmd(currentAddress.getAddress(hex(addr)), name)
if not cmd.applyTo(currentProgram, monitor):
print("Failed to apply demangled name to %s at %s due %s, falling back to mangled" % (name, hex(addr), cmd.getStatusMsg()))
createLabel(toAddr(addr), name, True)
class GhidraDisassemblerInterface(BaseDisassemblerInterface):
supports_fake_string_segment = False
def make_function(start, end = None):
addr = toAddr(start)
# Don't override existing functions
fn = getFunctionAt(addr)
if fn is None:
# Create new function if none exists
createFunction(addr, None)
def get_script_directory(self) -> str:
return getSourceFile().getParentFile().toString()
def make_array(addr, numItems, cppType):
if cppType.startswith('struct '):
cppType = cppType[7:]
t = getDataTypes(cppType)[0]
a = ArrayDataType(t, numItems, t.getLength())
addr = toAddr(addr)
removeDataAt(addr)
createData(addr, a)
def on_start(self):
self.xrefs = currentProgram.getReferenceManager()
def define_code(code):
# Code declarations are not supported in Ghidra
# This only affects string literals for metadata version < 19
# TODO: Replace with creating a DataType for enums
# Check that the user has parsed the C headers first
if len(getDataTypes('Il2CppObject')) == 0:
print('STOP! You must import the generated C header file (%TYPE_HEADER_RELATIVE_PATH%) before running this script.')
print('See https://github.com/djkaty/Il2CppInspector/blob/master/README.md#adding-metadata-to-your-ghidra-workflow for instructions.')
sys.exit()
# Ghidra sets the image base for ELF to 0x100000 for some reason
# https://github.com/NationalSecurityAgency/ghidra/issues/1020
# Make sure that the base address is 0
# Without this, Ghidra may not analyze the binary correctly and you will just waste your time
# If 0 doesn't work for you, replace it with the base address from the output of the CLI or GUI
if currentProgram.getExecutableFormat().endswith('(ELF)'):
currentProgram.setImageBase(toAddr(0), True)
# Don't trigger decompiler
setAnalysisOption(currentProgram, "Call Convention ID", "false")
def on_finish(self):
pass
def define_function(self, address: int, end: int | None = None):
address = toAddr(address)
# Don't override existing functions
fn = getFunctionAt(address)
if fn is None:
# Create new function if none exists
createFunction(address, None)
def define_data_array(self, address: int, type: str, count: int):
if type.startswith('struct '):
type = type[7:]
t = getDataTypes(type)[0]
a = ArrayDataType(t, count, t.getLength())
address = toAddr(address)
removeDataAt(address)
createData(address, a)
def set_data_type(self, address: int, type: str):
if type.startswith('struct '):
type = type[7:]
try:
t = getDataTypes(type)[0]
address = toAddr(address)
removeDataAt(address)
createData(address, t)
except:
print("Failed to set type: %s" % type)
def set_function_type(self, address: int, type: str):
make_function(address)
typeSig = CParserUtils.parseSignature(None, currentProgram, type)
ApplyFunctionSignatureCmd(toAddr(address), typeSig, SourceType.USER_DEFINED, False, True).applyTo(currentProgram)
def set_data_comment(self, address: int, cmt: str):
setEOLComment(toAddr(address), cmt)
def set_function_comment(self, address: int, cmt: str):
setPlateComment(toAddr(address), cmt)
def set_data_name(self, address: int, name: str):
address = toAddr(address)
if len(name) > 2000:
print("Name length exceeds 2000 characters, skipping (%s)" % name)
return
if not name.startswith("_ZN"):
createLabel(address, name, True)
return
cmd = DemanglerCmd(address, name)
if not cmd.applyTo(currentProgram, monitor):
print(f"Failed to apply demangled name to {name} at {address} due {cmd.getStatusMsg()}, falling back to mangled")
createLabel(address, name, True)
def set_function_name(self, address: int, name: str):
return self.set_data_name(address, name)
def add_cross_reference(self, from_address: int, to_address: int):
self.xrefs.addMemoryReference(toAddr(from_address), toAddr(to_address), RefType.DATA, SourceType.USER_DEFINED, 0)
def import_c_typedef(self, type_def: str):
# Code declarations are not supported in Ghidra
# This only affects string literals for metadata version < 19
# TODO: Replace with creating a DataType for enums
pass
class GhidraStatusHandler(BaseStatusHandler):
pass
def set_function_type(addr, sig):
make_function(addr)
typeSig = CParserUtils.parseSignature(None, currentProgram, sig)
ApplyFunctionSignatureCmd(toAddr(addr), typeSig, SourceType.USER_DEFINED, False, True).applyTo(currentProgram)
def set_type(addr, cppType):
if cppType.startswith('struct '):
cppType = cppType[7:]
try:
t = getDataTypes(cppType)[0]
addr = toAddr(addr)
removeDataAt(addr)
createData(addr, t)
except:
print("Failed to set type: %s" % cppType)
def set_comment(addr, text):
setEOLComment(toAddr(addr), text)
def set_header_comment(addr, text):
setPlateComment(toAddr(addr), text)
def script_prologue(status):
# Check that the user has parsed the C headers first
if len(getDataTypes('Il2CppObject')) == 0:
print('STOP! You must import the generated C header file (%TYPE_HEADER_RELATIVE_PATH%) before running this script.')
print('See https://github.com/djkaty/Il2CppInspector/blob/master/README.md#adding-metadata-to-your-ghidra-workflow for instructions.')
sys.exit()
# Ghidra sets the image base for ELF to 0x100000 for some reason
# https://github.com/NationalSecurityAgency/ghidra/issues/1020
# Make sure that the base address is 0
# Without this, Ghidra may not analyze the binary correctly and you will just waste your time
# If 0 doesn't work for you, replace it with the base address from the output of the CLI or GUI
if currentProgram.getExecutableFormat().endswith('(ELF)'):
currentProgram.setImageBase(toAddr(0), True)
# Don't trigger decompiler
setAnalysisOption(currentProgram, "Call Convention ID", "false")
def get_script_directory(): return getSourceFile().getParentFile().toString()
def script_epilogue(status): pass
def add_function_to_group(addr, group): pass
def add_xref(addr, to):
xrefs.addMemoryReference(currentAddress.getAddress(hex(addr)), currentAddress.getAddress(hex(to)), RefType.DATA, SourceType.USER_DEFINED, 0)
def process_string_literals(status, data):
for d in jsonData['stringLiterals']:
define_string(d)
# I don't know how to make inline strings in Ghidra
# Just revert back original impl
addr = parse_address(d)
set_name(addr, d['name'])
set_type(addr, r'struct String *')
set_comment(addr, d['string'])
status.update_progress()
class StatusHandler(BaseStatusHandler): pass
status = GhidraStatusHandler()
backend = GhidraDisassemblerInterface()
context = ScriptContext(backend, status)
context.process()

View File

@@ -8,6 +8,8 @@ import ida_nalt
import ida_ida
import ida_ua
import ida_segment
import ida_funcs
import ida_xref
try: # 7.7+
import ida_srclang
@@ -23,206 +25,205 @@ try:
except ImportError:
FOLDERS_AVAILABLE = False
cached_genflags = 0
skip_make_function = False
func_dirtree = None
is_32_bit = False
fake_segments_base = None
def script_prologue(status):
global cached_genflags, skip_make_function, func_dirtree, is_32_bit, fake_segments_base
# Disable autoanalysis
cached_genflags = ida_ida.inf_get_genflags()
ida_ida.inf_set_genflags(cached_genflags & ~ida_ida.INFFL_AUTO)
# Unload type libraries we know to cause issues - like the c++ linux one
PLATFORMS = ["x86", "x64", "arm", "arm64"]
PROBLEMATIC_TYPELIBS = ["gnulnx"]
for lib in PROBLEMATIC_TYPELIBS:
for platform in PLATFORMS:
ida_typeinf.del_til(f"{lib}_{platform}")
# Set name mangling to GCC 3.x and display demangled as default
ida_ida.inf_set_demnames(ida_ida.DEMNAM_GCC3 | ida_ida.DEMNAM_NAME)
status.update_step('Processing Types')
if IDACLANG_AVAILABLE:
header_path = os.path.join(get_script_directory(), "%TYPE_HEADER_RELATIVE_PATH%")
ida_srclang.set_parser_argv("clang", "-target x86_64-pc-linux -x c++ -D_IDACLANG_=1") # -target required for 8.3+
ida_srclang.parse_decls_with_parser("clang", None, header_path, True)
else:
original_macros = ida_typeinf.get_c_macros()
ida_typeinf.set_c_macros(original_macros + ";_IDA_=1")
ida_typeinf.idc_parse_types(os.path.join(get_script_directory(), "%TYPE_HEADER_RELATIVE_PATH%"), ida_typeinf.PT_FILE)
ida_typeinf.set_c_macros(original_macros)
# Skip make_function on Windows GameAssembly.dll files due to them predefining all functions through pdata which makes the method very slow
skip_make_function = ida_segment.get_segm_by_name(".pdata") is not None
if skip_make_function:
print(".pdata section found, skipping function boundaries")
if FOLDERS_AVAILABLE:
func_dirtree = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
is_32_bit = ida_ida.inf_is_32bit_exactly()
def script_epilogue(status):
# Reenable auto-analysis
global cached_genflags
ida_ida.inf_set_genflags(cached_genflags)
# Utility methods
def set_name(addr, name):
ida_name.set_name(addr, name, ida_name.SN_NOWARN | ida_name.SN_NOCHECK | ida_name.SN_FORCE)
def make_function(start, end = None):
global skip_make_function
if skip_make_function:
return
ida_bytes.del_items(start, ida_bytes.DELIT_SIMPLE, 12) # Undefine x bytes which should hopefully be enough for the first instruction
ida_ua.create_insn(start) # Create instruction at start
if not ida_funcs.add_func(start, end if end is not None else ida_idaapi.BADADDR): # This fails if the function doesn't start with an instruction
print(f"failed to mark function {hex(start)}-{hex(end) if end is not None else '???'} as function")
TYPE_CACHE = {}
def get_type(typeName):
if typeName not in TYPE_CACHE:
info = ida_typeinf.idc_parse_decl(None, typeName, ida_typeinf.PT_RAWARGS)
if info is None:
print(f"Failed to create type {typeName}.")
return None
TYPE_CACHE[typeName] = info[1:]
return TYPE_CACHE[typeName]
#try:
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# from ..shared_base import BaseStatusHandler, BaseDisassemblerInterface, ScriptContext
# import json
# import os
# from datetime import datetime
#except:
# pass
TINFO_DEFINITE = 0x0001 # These only exist in idc for some reason, so we redefine it here
DEFAULT_TIL: "til_t" = None # type: ignore
def set_type(addr, cppType):
cppType += ';'
class IDADisassemblerInterface(BaseDisassemblerInterface):
supports_fake_string_segment = True
info = get_type(cppType)
if info is None:
return
_status: BaseStatusHandler
if ida_typeinf.apply_type(None, info[0], info[1], addr, TINFO_DEFINITE) is None:
print(f"set_type({hex(addr)}, {cppType}); failed!")
def set_function_type(addr, sig):
set_type(addr, sig)
def make_array(addr, numItems, cppType):
set_type(addr, cppType)
flags = ida_bytes.get_flags(addr)
if ida_bytes.is_struct(flags):
opinfo = ida_nalt.opinfo_t()
ida_bytes.get_opcode(opinfo, addr, 0, flags)
entrySize = ida_bytes.get_data_elsize(addr, flags, opinfo)
tid = opinfo.tid
else:
entrySize = ida_bytes.get_item_size(addr)
tid = ida_idaapi.BADADDR
ida_bytes.create_data(addr, flags, numItems * entrySize, tid)
def define_code(code):
ida_typeinf.idc_parse_types(code)
def set_comment(addr, comment, repeatable = True):
ida_bytes.set_cmt(addr, comment, repeatable)
def set_header_comment(addr, comment):
func = ida_funcs.get_func(addr)
if func is None:
return
ida_funcs.set_func_cmt(func, comment, True)
def get_script_directory():
return os.path.dirname(os.path.realpath(__file__))
folders = []
def add_function_to_group(addr, group):
global func_dirtree, folders
return
if not FOLDERS_AVAILABLE:
return
if group not in folders:
folders.append(group)
func_dirtree.mkdir(group)
name = ida_funcs.get_func_name(addr)
func_dirtree.rename(name, f"{group}/{name}")
def add_xref(addr, to):
ida_xref.add_dref(addr, to, ida_xref.XREF_USER | ida_xref.dr_I)
def write_string(addr, string):
encoded_string = string.encode() + b'\x00'
string_length = len(encoded_string)
ida_bytes.put_bytes(addr, encoded_string)
ida_bytes.create_strlit(addr, string_length, ida_nalt.STRTYPE_C)
def write_address(addr, value):
global is_32_bit
if is_32_bit:
ida_bytes.put_dword(addr, value)
else:
ida_bytes.put_qword(addr, value)
def create_fake_segment(name, size):
global is_32_bit
start = ida_ida.inf_get_max_ea()
end = start + size
ida_segment.add_segm(0, start, end, name, "DATA")
segment = ida_segment.get_segm_by_name(name)
segment.bitness = 1 if is_32_bit else 2
segment.perm = ida_segment.SEGPERM_READ
segment.update()
return start
def process_string_literals(status, data):
total_string_length = 0
for d in data['stringLiterals']:
total_string_length += len(d["string"]) + 1
_type_cache: dict
_folders: list
aligned_length = total_string_length + (4096 - (total_string_length % 4096))
segment_base = create_fake_segment(".fake_strings", aligned_length)
_function_dirtree: "ida_dirtree.dirtree_t"
_cached_genflags: int
_skip_function_creation: bool
_is_32_bit: bool
_fake_segments_base: int
current_string_address = segment_base
for d in data['stringLiterals']:
define_string(d)
def __init__(self, status: BaseStatusHandler):
self._status = status
self._type_cache = {}
self._folders = []
ref_addr = parse_address(d)
write_string(current_string_address, d["string"])
write_address(ref_addr, current_string_address)
set_type(ref_addr, r'const char* const')
self._cached_genflags = 0
self._skip_function_creation = False
self._is_32_bit = False
self._fake_segments_base = 0
current_string_address += len(d["string"]) + 1
status.update_progress()
def _get_type(self, type: str):
if type not in self._type_cache:
info = ida_typeinf.idc_parse_decl(DEFAULT_TIL, type, ida_typeinf.PT_RAWARGS)
if info is None:
print(f"Failed to create type {type}.")
return None
self._type_cache[type] = info[1:]
return self._type_cache[type]
def get_script_directory(self) -> str:
return os.path.dirname(os.path.realpath(__file__))
def on_start(self):
# Disable autoanalysis
self._cached_genflags = ida_ida.inf_get_genflags()
ida_ida.inf_set_genflags(self._cached_genflags & ~ida_ida.INFFL_AUTO)
# Unload type libraries we know to cause issues - like the c++ linux one
PLATFORMS = ["x86", "x64", "arm", "arm64"]
PROBLEMATIC_TYPELIBS = ["gnulnx"]
for lib in PROBLEMATIC_TYPELIBS:
for platform in PLATFORMS:
ida_typeinf.del_til(f"{lib}_{platform}")
# Set name mangling to GCC 3.x and display demangled as default
ida_ida.inf_set_demnames(ida_ida.DEMNAM_GCC3 | ida_ida.DEMNAM_NAME)
self._status.update_step('Processing Types')
if IDACLANG_AVAILABLE:
header_path = os.path.join(self.get_script_directory(), "%TYPE_HEADER_RELATIVE_PATH%")
ida_srclang.set_parser_argv("clang", "-target x86_64-pc-linux -x c++ -D_IDACLANG_=1") # -target required for 8.3+
ida_srclang.parse_decls_with_parser("clang", None, header_path, True)
else:
original_macros = ida_typeinf.get_c_macros()
ida_typeinf.set_c_macros(original_macros + ";_IDA_=1")
ida_typeinf.idc_parse_types(os.path.join(self.get_script_directory(), "%TYPE_HEADER_RELATIVE_PATH%"), ida_typeinf.PT_FILE)
ida_typeinf.set_c_macros(original_macros)
# Skip make_function on Windows GameAssembly.dll files due to them predefining all functions through pdata which makes the method very slow
skip_make_function = ida_segment.get_segm_by_name(".pdata") is not None
if skip_make_function:
print(".pdata section found, skipping function boundaries")
if FOLDERS_AVAILABLE:
self._function_dirtree = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
self._is_32_bit = ida_ida.inf_is_32bit_exactly()
def on_finish(self):
ida_ida.inf_set_genflags(self._cached_genflags)
def define_function(self, address: int, end: int | None = None):
if self._skip_function_creation:
return
ida_bytes.del_items(address, ida_bytes.DELIT_SIMPLE, 12) # Undefine x bytes which should hopefully be enough for the first instruction
ida_ua.create_insn(address) # Create instruction at start
if not ida_funcs.add_func(address, end if end is not None else ida_idaapi.BADADDR): # This fails if the function doesn't start with an instruction
print(f"failed to mark function {hex(address)}-{hex(end) if end is not None else '???'} as function")
def define_data_array(self, address: int, type: str, count: int):
self.set_data_type(address, type)
flags = ida_bytes.get_flags(address)
if ida_bytes.is_struct(flags):
opinfo = ida_nalt.opinfo_t()
ida_bytes.get_opinfo(opinfo, address, 0, flags)
entrySize = ida_bytes.get_data_elsize(address, flags, opinfo)
tid = opinfo.tid
else:
entrySize = ida_bytes.get_item_size(address)
tid = ida_idaapi.BADADDR
ida_bytes.create_data(address, flags, count * entrySize, tid)
def set_data_type(self, address: int, type: str):
type += ';'
info = self._get_type(type)
if info is None:
return
if ida_typeinf.apply_type(DEFAULT_TIL, info[0], info[1], address, TINFO_DEFINITE) is None:
print(f"set_type({hex(address)}, {type}); failed!")
def set_function_type(self, address: int, type: str):
self.set_data_type(address, type)
def set_data_comment(self, address: int, cmt: str):
ida_bytes.set_cmt(address, cmt, False)
def set_function_comment(self, address: int, cmt: str):
func = ida_funcs.get_func(address)
if func is None:
return
ida_funcs.set_func_cmt(func, cmt, True)
def set_data_name(self, address: int, name: str):
ida_name.set_name(address, name, ida_name.SN_NOWARN | ida_name.SN_NOCHECK | ida_name.SN_FORCE)
def set_function_name(self, address: int, name: str):
self.set_data_name(address, name)
def add_cross_reference(self, from_address: int, to_address: int):
ida_xref.add_dref(from_address, to_address, ida_xref.XREF_USER | ida_xref.dr_I)
def import_c_typedef(self, type_def: str):
ida_typeinf.idc_parse_types(type_def, 0)
# optional
def add_function_to_group(self, address: int, group: str):
if not FOLDERS_AVAILABLE or True: # enable at your own risk - this is slow
return
if group not in self._folders:
self._folders.append(group)
self._function_dirtree.mkdir(group)
name = ida_funcs.get_func_name(address)
self._function_dirtree.rename(name, f"{group}/{name}")
# only required if supports_fake_string_segment == True
def create_fake_segment(self, name: str, size: int) -> int:
start = ida_ida.inf_get_max_ea()
end = start + size
ida_segment.add_segm(0, start, end, name, "DATA")
segment = ida_segment.get_segm_by_name(name)
segment.bitness = 1 if self._is_32_bit else 2
segment.perm = ida_segment.SEGPERM_READ
segment.update()
return start
def write_string(self, address: int, value: str):
encoded_string = value.encode() + b'\x00'
string_length = len(encoded_string)
ida_bytes.put_bytes(address, encoded_string)
ida_bytes.create_strlit(address, string_length, ida_nalt.STRTYPE_C)
def write_address(self, address: int, value: int):
if self._is_32_bit:
ida_bytes.put_dword(address, value)
else:
ida_bytes.put_qword(address, value)
# Status handler
class StatusHandler(BaseStatusHandler):
class IDAStatusHandler(BaseStatusHandler):
def __init__(self):
self.step = "Initializing"
self.max_items = 0
self.current_items = 0
self.start_time = datetime.datetime.now()
self.start_time = datetime.now()
self.step_start_time = self.start_time
self.last_updated_time = datetime.datetime.min
self.last_updated_time = datetime.min
def initialize(self):
ida_kernwin.show_wait_box("Processing")
@@ -231,7 +232,7 @@ class StatusHandler(BaseStatusHandler):
if self.was_cancelled():
raise RuntimeError("Cancelled script.")
current_time = datetime.datetime.now()
current_time = datetime.now()
if 0.5 > (current_time - self.last_updated_time).total_seconds():
return
@@ -254,8 +255,8 @@ Elapsed: {step_time} ({total_time})
self.step = step
self.max_items = max_items
self.current_items = 0
self.step_start_time = datetime.datetime.now()
self.last_updated_time = datetime.datetime.min
self.step_start_time = datetime.now()
self.last_updated_time = datetime.min
self.update()
def update_progress(self, new_progress = 1):
@@ -265,5 +266,10 @@ Elapsed: {step_time} ({total_time})
def was_cancelled(self):
return ida_kernwin.user_cancelled()
def close(self):
ida_kernwin.hide_wait_box()
def shutdown(self):
ida_kernwin.hide_wait_box()
status = IDAStatusHandler()
backend = IDADisassemblerInterface(status)
context = ScriptContext(backend, status)
context.process()

View File

@@ -1,182 +0,0 @@
# Shared interface
def from_hex(addr): return int(addr, 0)
def parse_address(d): return from_hex(d['virtualAddress'])
def define_il_method(jsonDef):
addr = parse_address(jsonDef)
set_name(addr, jsonDef['name'])
set_function_type(addr, jsonDef['signature'])
set_header_comment(addr, jsonDef['dotNetSignature'])
add_function_to_group(addr, jsonDef['group'])
def define_il_method_info(jsonDef):
addr = parse_address(jsonDef)
set_name(addr, jsonDef['name'])
set_comment(addr, jsonDef['dotNetSignature'])
set_type(addr, r'struct MethodInfo *')
if 'methodAddress' in jsonDef:
add_xref(from_hex(jsonDef["methodAddress"]), addr)
def define_cpp_function(jsonDef):
addr = parse_address(jsonDef)
set_name(addr, jsonDef['name'])
set_function_type(addr, jsonDef['signature'])
def define_string(jsonDef):
addr = parse_address(jsonDef)
set_name(addr, jsonDef['name'])
set_comment(addr, jsonDef['string'])
def define_field(addr, name, type, ilType = None):
addr = from_hex(addr)
set_name(addr, name)
set_type(addr, type)
if ilType is not None:
set_comment(addr, ilType)
def define_field_from_json(jsonDef):
define_field(jsonDef['virtualAddress'], jsonDef['name'], jsonDef['type'], jsonDef['dotNetType'])
def define_array(jsonDef):
addr = parse_address(jsonDef)
make_array(addr, int(jsonDef['count']), jsonDef['type'])
set_name(addr, jsonDef['name'])
def define_field_with_value(jsonDef):
addr = parse_address(jsonDef)
set_name(addr, jsonDef['name'])
set_comment(addr, jsonDef['value'])
# Process JSON
def process_json(jsonData, status):
# Function boundaries
functionAddresses = jsonData['functionAddresses']
functionAddresses.sort()
count = len(functionAddresses)
status.update_step('Processing function boundaries', count)
for i in range(count):
start = from_hex(functionAddresses[i])
if start == 0:
status.update_progress()
continue
end = from_hex(functionAddresses[i + 1]) if i + 1 != count else None
make_function(start, end)
status.update_progress()
# Method definitions
status.update_step('Processing method definitions', len(jsonData['methodDefinitions']))
for d in jsonData['methodDefinitions']:
define_il_method(d)
status.update_progress()
# Constructed generic methods
status.update_step('Processing constructed generic methods', len(jsonData['constructedGenericMethods']))
for d in jsonData['constructedGenericMethods']:
define_il_method(d)
status.update_progress()
# Custom attributes generators
status.update_step('Processing custom attributes generators', len(jsonData['customAttributesGenerators']))
for d in jsonData['customAttributesGenerators']:
define_cpp_function(d)
status.update_progress()
# Method.Invoke thunks
status.update_step('Processing Method.Invoke thunks', len(jsonData['methodInvokers']))
for d in jsonData['methodInvokers']:
define_cpp_function(d)
status.update_progress()
# String literals for version >= 19
if 'virtualAddress' in jsonData['stringLiterals'][0]:
status.update_step('Processing string literals (V19+)', len(jsonData['stringLiterals']))
process_string_literals(status, jsonData)
# String literals for version < 19
else:
status.update_step('Processing string literals (pre-V19)')
litDecl = 'enum StringLiteralIndex {\n'
for d in jsonData['stringLiterals']:
litDecl += " " + d['name'] + ",\n"
litDecl += '};\n'
define_code(litDecl)
# Il2CppClass (TypeInfo) pointers
status.update_step('Processing Il2CppClass (TypeInfo) pointers', len(jsonData['typeInfoPointers']))
for d in jsonData['typeInfoPointers']:
define_field_from_json(d)
status.update_progress()
# Il2CppType (TypeRef) pointers
status.update_step('Processing Il2CppType (TypeRef) pointers', len(jsonData['typeRefPointers']))
for d in jsonData['typeRefPointers']:
define_field(d['virtualAddress'], d['name'], r'struct Il2CppType *', d['dotNetType'])
status.update_progress()
# MethodInfo pointers
status.update_step('Processing MethodInfo pointers', len(jsonData['methodInfoPointers']))
for d in jsonData['methodInfoPointers']:
define_il_method_info(d)
status.update_progress()
# FieldInfo pointers, add the contents as a comment
status.update_step('Processing FieldInfo pointers', len(jsonData['fields']))
for d in jsonData['fields']:
define_field_with_value(d)
status.update_progress()
# FieldRva pointers, add the contents as a comment
status.update_step('Processing FieldRva pointers', len(jsonData['fieldRvas']))
for d in jsonData['fieldRvas']:
define_field_with_value(d)
status.update_progress()
# IL2CPP type metadata
status.update_step('Processing IL2CPP type metadata', len(jsonData['typeMetadata']))
for d in jsonData['typeMetadata']:
define_field(d['virtualAddress'], d['name'], d['type'])
# IL2CPP function metadata
status.update_step('Processing IL2CPP function metadata', len(jsonData['functionMetadata']))
for d in jsonData['functionMetadata']:
define_cpp_function(d)
# IL2CPP array metadata
status.update_step('Processing IL2CPP array metadata', len(jsonData['arrayMetadata']))
for d in jsonData['arrayMetadata']:
define_array(d)
# IL2CPP API functions
status.update_step('Processing IL2CPP API functions', len(jsonData['apis']))
for d in jsonData['apis']:
define_cpp_function(d)
# Entry point
print('Generated script file by Il2CppInspectorRedux - https://github.com/LukeFZ (Original Il2CppInspector by http://www.djkaty.com - https://github.com/djkaty)')
status = StatusHandler()
status.initialize()
try:
start_time = datetime.datetime.now()
status.update_step("Running script prologue")
script_prologue(status)
with open(os.path.join(get_script_directory(), "%JSON_METADATA_RELATIVE_PATH%"), "r") as jsonFile:
status.update_step("Loading JSON metadata")
jsonData = json.load(jsonFile)['addressMap']
process_json(jsonData, status)
status.update_step("Running script epilogue")
script_epilogue(status)
status.update_step('Script execution complete.')
print("Took: %s" % (datetime.datetime.now() - start_time))
except RuntimeError: pass
finally: status.close()

View File

@@ -1,14 +0,0 @@
# Generated script file by Il2CppInspectorRedux - https://github.com/LukeFZ (Original Il2CppInspector by http://www.djkaty.com - https://github.com/djkaty)
# Target Unity version: %TARGET_UNITY_VERSION%
import json
import os
import sys
import datetime
class BaseStatusHandler:
def initialize(self): pass
def update_step(self, name, max_items = 0): print(name)
def update_progress(self, progress = 1): pass
def was_cancelled(self): return False
def close(self): pass

View File

@@ -0,0 +1,290 @@
# Generated script file by Il2CppInspectorRedux - https://github.com/LukeFZ (Original Il2CppInspector by http://www.djkaty.com - https://github.com/djkaty)
# Target Unity version: %TARGET_UNITY_VERSION%
import json
import os
from datetime import datetime
import abc
class BaseStatusHandler(abc.ABC):
def initialize(self): pass
def shutdown(self): pass
def update_step(self, name: str, max_items: int = 0): print(name)
def update_progress(self, progress: int = 1): pass
def was_cancelled(self): return False
class BaseDisassemblerInterface(abc.ABC):
supports_fake_string_segment: bool = False
@abc.abstractmethod
def get_script_directory(self) -> str: return ""
@abc.abstractmethod
def on_start(self): pass
@abc.abstractmethod
def on_finish(self): pass
@abc.abstractmethod
def define_function(self, address: int, end: int | None = None): pass
@abc.abstractmethod
def define_data_array(self, address: int, type: str, count: int): pass
@abc.abstractmethod
def set_data_type(self, address: int, type: str): pass
@abc.abstractmethod
def set_function_type(self, address: int, type: str): pass
@abc.abstractmethod
def set_data_comment(self, address: int, cmt: str): pass
@abc.abstractmethod
def set_function_comment(self, address: int, cmt: str): pass
@abc.abstractmethod
def set_data_name(self, address: int, name: str): pass
@abc.abstractmethod
def set_function_name(self, address: int, name: str): pass
@abc.abstractmethod
def add_cross_reference(self, from_address: int, to_address: int): pass
@abc.abstractmethod
def import_c_typedef(self, type_def: str): pass
# optional
def add_function_to_group(self, address: int, group: str): pass
def cache_function_types(self, function_types: list[str]): pass
# only required if supports_fake_string_segment == True
def create_fake_segment(self, name: str, size: int) -> int: return 0
def write_string(self, address: int, value: str): pass
def write_address(self, address: int, value: int): pass
class ScriptContext:
_backend: BaseDisassemblerInterface
_status: BaseStatusHandler
def __init__(self, backend: BaseDisassemblerInterface, status: BaseStatusHandler) -> None:
self._backend = backend
self._status = status
def from_hex(self, addr: str):
return int(addr, 0)
def parse_address(self, d: dict):
return self.from_hex(d['virtualAddress'])
def define_il_method(self, definition: dict):
addr = self.parse_address(definition)
self._backend.set_function_name(addr, definition['name'])
self._backend.set_function_type(addr, definition['signature'])
self._backend.set_function_comment(addr, definition['dotNetSignature'])
self._backend.add_function_to_group(addr, definition['group'])
def define_il_method_info(self, definition: dict):
addr = self.parse_address(definition)
self._backend.set_data_type(addr, r'struct MethodInfo *')
self._backend.set_data_name(addr, definition['name'])
self._backend.set_data_comment(addr, definition['dotNetSignature'])
if 'methodAddress' in definition:
method_addr = self.from_hex(definition["methodAddress"])
self._backend.add_cross_reference(method_addr, addr)
def define_cpp_function(self, definition: dict):
addr = self.parse_address(definition)
self._backend.set_function_name(addr, definition['name'])
self._backend.set_function_type(addr, definition['signature'])
def define_string(self, definition: dict):
addr = self.parse_address(definition)
self._backend.set_data_type(addr, r'struct String *')
self._backend.set_data_name(addr, definition['name'])
self._backend.set_data_comment(addr, definition['string'])
def define_field(self, addr: str, name: str, type: str, il_type: str | None = None):
address = self.from_hex(addr)
self._backend.set_data_type(address, type)
self._backend.set_data_name(address, name)
if il_type is not None:
self._backend.set_data_comment(address, il_type)
def define_field_from_json(self, definition: dict):
self.define_field(definition['virtualAddress'], definition['name'], definition['type'], definition['dotNetType'])
def define_array(self, definition: dict):
addr = self.parse_address(definition)
self._backend.define_data_array(addr, definition['type'], int(definition['count']))
self._backend.set_data_name(addr, definition['name'])
def define_field_with_value(self, definition: dict):
addr = self.parse_address(definition)
self._backend.set_data_name(addr, definition['name'])
self._backend.set_data_comment(addr, definition['value'])
def process_metadata(self, metadata: dict):
# Function boundaries
function_addresses = metadata['functionAddresses']
function_addresses.sort()
count = len(function_addresses)
self._status.update_step('Processing function boundaries', count)
for i in range(count):
start = self.from_hex(function_addresses[i])
if start == 0:
self._status.update_progress()
continue
end = self.from_hex(function_addresses[i + 1]) if i + 1 != count else None
self._backend.define_function(start, end)
self._status.update_progress()
# Method definitions
self._status.update_step('Processing method definitions', len(metadata['methodDefinitions']))
self._backend.cache_function_types([x["signature"] for x in metadata['methodDefinitions']])
for d in metadata['methodDefinitions']:
self.define_il_method(d)
self._status.update_progress()
# Constructed generic methods
self._status.update_step('Processing constructed generic methods', len(metadata['constructedGenericMethods']))
self._backend.cache_function_types([x["signature"] for x in metadata['constructedGenericMethods']])
for d in metadata['constructedGenericMethods']:
self.define_il_method(d)
self._status.update_progress()
# Custom attributes generators
self._status.update_step('Processing custom attributes generators', len(metadata['customAttributesGenerators']))
self._backend.cache_function_types([x["signature"] for x in metadata['customAttributesGenerators']])
for d in metadata['customAttributesGenerators']:
self.define_cpp_function(d)
self._status.update_progress()
# Method.Invoke thunks
self._status.update_step('Processing Method.Invoke thunks', len(metadata['methodInvokers']))
self._backend.cache_function_types([x["signature"] for x in metadata['methodInvokers']])
for d in metadata['methodInvokers']:
self.define_cpp_function(d)
self._status.update_progress()
# String literals for version >= 19
if 'virtualAddress' in metadata['stringLiterals'][0]:
self._status.update_step('Processing string literals (V19+)', len(metadata['stringLiterals']))
if self._backend.supports_fake_string_segment:
total_string_length = 0
for d in metadata['stringLiterals']:
total_string_length += len(d["string"]) + 1
aligned_length = total_string_length + (4096 - (total_string_length % 4096))
segment_base = self._backend.create_fake_segment(".fake_strings", aligned_length)
current_string_address = segment_base
for d in metadata['stringLiterals']:
self.define_string(d)
ref_addr = self.parse_address(d)
self._backend.write_string(current_string_address, d["string"])
self._backend.set_data_type(ref_addr, r'const char* const')
self._backend.write_address(ref_addr, current_string_address)
current_string_address += len(d["string"]) + 1
self._status.update_progress()
else:
for d in metadata['stringLiterals']:
self.define_string(d)
self._status.update_progress()
# String literals for version < 19
else:
self._status.update_step('Processing string literals (pre-V19)')
litDecl = 'enum StringLiteralIndex {\n'
for d in metadata['stringLiterals']:
litDecl += " " + d['name'] + ",\n"
litDecl += '};\n'
self._backend.import_c_typedef(litDecl)
# Il2CppClass (TypeInfo) pointers
self._status.update_step('Processing Il2CppClass (TypeInfo) pointers', len(metadata['typeInfoPointers']))
for d in metadata['typeInfoPointers']:
self.define_field_from_json(d)
self._status.update_progress()
# Il2CppType (TypeRef) pointers
self._status.update_step('Processing Il2CppType (TypeRef) pointers', len(metadata['typeRefPointers']))
for d in metadata['typeRefPointers']:
self.define_field(d['virtualAddress'], d['name'], r'struct Il2CppType *', d['dotNetType'])
self._status.update_progress()
# MethodInfo pointers
self._status.update_step('Processing MethodInfo pointers', len(metadata['methodInfoPointers']))
for d in metadata['methodInfoPointers']:
self.define_il_method_info(d)
self._status.update_progress()
# FieldInfo pointers, add the contents as a comment
self._status.update_step('Processing FieldInfo pointers', len(metadata['fields']))
for d in metadata['fields']:
self.define_field_with_value(d)
self._status.update_progress()
# FieldRva pointers, add the contents as a comment
self._status.update_step('Processing FieldRva pointers', len(metadata['fieldRvas']))
for d in metadata['fieldRvas']:
self.define_field_with_value(d)
self._status.update_progress()
# IL2CPP type metadata
self._status.update_step('Processing IL2CPP type metadata', len(metadata['typeMetadata']))
for d in metadata['typeMetadata']:
self.define_field(d['virtualAddress'], d['name'], d['type'])
# IL2CPP function metadata
self._status.update_step('Processing IL2CPP function metadata', len(metadata['functionMetadata']))
for d in metadata['functionMetadata']:
self.define_cpp_function(d)
# IL2CPP array metadata
self._status.update_step('Processing IL2CPP array metadata', len(metadata['arrayMetadata']))
for d in metadata['arrayMetadata']:
self.define_array(d)
# IL2CPP API functions
self._status.update_step('Processing IL2CPP API functions', len(metadata['apis']))
self._backend.cache_function_types([x["signature"] for x in metadata['apis']])
for d in metadata['apis']:
self.define_cpp_function(d)
def process(self):
self._status.initialize()
try:
start_time = datetime.now()
self._status.update_step("Running script prologue")
self._backend.on_start()
metadata_path = os.path.join(self._backend.get_script_directory(), "%JSON_METADATA_RELATIVE_PATH%")
with open(metadata_path, "r") as f:
self._status.update_step("Loading JSON metadata")
metadata = json.load(f)['addressMap']
self.process_metadata(metadata)
self._status.update_step("Running script epilogue")
self._backend.on_finish()
self._status.update_step('Script execution complete.')
end_time = datetime.now()
print(f"Took: {end_time - start_time}")
except RuntimeError: pass
finally: self._status.shutdown()