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
This commit is contained in:
Luke
2024-11-08 23:31:40 +01:00
committed by GitHub
parent 792268f52f
commit 5715760e8b
9 changed files with 912 additions and 510 deletions

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");
}

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()

View File

@@ -27,6 +27,7 @@ This is a continuation of [Il2CppInspector, by djkaty](https://github.com/djkaty
- Automatic unloading of conflicting type libraries
- Addition of custom fake string segment to show string literal contents in decompiler
- A fake xref between MethodInfo instances and their corresponding method to quickly get the correct function
* Binary Ninja script output, with all of the IDA-exclusive features
### Main features
@@ -36,7 +37,7 @@ This is a continuation of [Il2CppInspector, by djkaty](https://github.com/djkaty
* Create **[C++ scaffolding](#creating-c-scaffolding-or-a-dll-injection-project)** for all types, methods, function pointers and API functions in an IL2CPP application for use in x64dbg, Cydia Substrate etc.
* Create **[IDA](#adding-metadata-to-your-ida-workflow) and [Ghidra](#adding-metadata-to-your-ghidra-workflow) Python scripts** to populate symbol, function and type information; includes API hooks to [implement scripts for other targets](#extending-il2cppinspectors-python-output-to-support-other-targets)
* Create **[IDA](#adding-metadata-to-your-ida-workflow), [Ghidra](#adding-metadata-to-your-ghidra-workflow) or [Binary Ninja](#adding-metadata-to-your-binary-ninja-workflow) Python scripts** to populate symbol, function and type information; includes API hooks to [implement scripts for other targets](#extending-il2cppinspectors-python-output-to-support-other-targets)
* Create Visual Studio **[C++ DLL injection projects](#dll-injection-workflow)** directly from IL2CPP files
@@ -68,7 +69,6 @@ This is a continuation of [Il2CppInspector, by djkaty](https://github.com/djkaty
* Tested with [every release of IL2CPP](#version-support) since Unity 5.3.0
### Tutorials and Guides
You can read more about how IL2CPP works in my series IL2CPP Reverse Engineering:
@@ -124,7 +124,7 @@ Nice to have:
* Automatically defeats certain basic obfuscation methods
* Test chassis for automated integration testing of IL2CPP binaries
Class library targets .NET 8. Built with Visual Studio 2019.
Class library targets .NET 8. Built with Visual Studio 2022.
**NOTE**: Il2CppInspector is not a decompiler. It can provide you with the structure of an application and function addresses for every method so that you can easily jump straight to methods of interest in your disassembler. It does not attempt to recover the entire source code of the application.
@@ -132,7 +132,7 @@ Class library targets .NET 8. Built with Visual Studio 2019.
```
git clone --recursive https://github.com/LukeFZ/Il2CppInspectorRedux
cd Il2CppInspector
cd Il2CppInspectorRedux
```
##### Windows
@@ -181,9 +181,9 @@ Get all current plugins (optional):
For other operating systems supporting .NET Core, add `-r xxx` to the final command where `xxx` is a RID from https://docs.microsoft.com/en-us/dotnet/articles/core/rid-catalog
The output binary for command-line usage is placed in `Il2CppInspector/Il2CppInspector.CLI/bin/Release/net8.0/[win|osx|linux]-x64/publish/Il2CppInspector.exe`.
The output binary for command-line usage is placed in `Il2CppInspectorRedux/Il2CppInspector.CLI/bin/Release/net8.0/[win|osx|linux]-x64/publish/Il2CppInspector.exe`.
The output binary for Windows GUI is placed in `Il2CppInspector/Il2CppInspector.GUI/bin/Release/net8.0-windows/win-x64/publish/Il2CppInspector.exe`.
The output binary for Windows GUI is placed in `Il2CppInspectorRedux/Il2CppInspector.GUI/bin/Release/net8.0-windows/win-x64/publish/Il2CppInspector.exe`.
The `plugins` folder should be placed in the same folder as `Il2CppInspector.exe`.
@@ -389,6 +389,12 @@ Example Ghidra C++ decompilation after applying Il2CppInspector:
![Il2CppInspector annotated Ghidra project](docs/Ghidra_Preview.png)
### Adding metadata to your Binary Ninja workflow
Import your binary into Binary Ninja, and let the initial analysis complete.
Then run the generated *il2cpp.py* using the "File > Run script..." menu option.
You can view the current script progress in the bottom left corner, alongside the total elapsed time.
### Creating C++ scaffolding or a DLL injection project
Il2CppInspector generates a series of C++ source files which you can use in a variety of ways, for example:
@@ -706,23 +712,14 @@ You can find out more about plugins, and browse the source code of current plugi
### Extending Il2CppInspector's Python output to support other targets
The current version of Il2CppInspector can output Python scripts targeting the IDA and Ghidra disassemblers.
The current version of Il2CppInspector can output Python scripts targeting the IDA, Ghidra and Binary Ninja disassemblers.
When Il2CppInspector generates such a script, it generates a concatenation of a shared block of code (`Outputs/ScriptResources/shared-main.py`) which parses the JSON metadata and dispatches it to a set of implementation-specific functions to be processed, and a block of code specific to the target application which implements these functions (a file from `Outputs/ScriptResources/Targets`).
When Il2CppInspector generates such a script, it generates a concatenation of a shared block of code (`Outputs/ScriptResources/shared_base_.py`) which parses the JSON metadata and dispatches it to a set of implementation-specific functions to be processed, and a block of code specific to the target application which implements these functions (a file from `Outputs/ScriptResources/Targets`).
If you would like to add support for a new target application, create a new Python script in `Outputs/ScriptResources/Targets` with the nane `<target-name-without-whitespace>.py` and implement the following functions:
- `CustomInitializer()` - perform any custom initialization required for the target before applying the metadata
- `DefineCode(code)` - parse and apply the specified C++ declaration text (this is not required for Unity 5.3.2 and later; if you don't need to support earlier versions, just specify `pass` as the implementation)
- `GetScriptDirectory()` - retrieve the directory that the Python script is running in. This will normally be `os.path.dirname(os.path.realpath(__file__))`
- `MakeFunction(start, name=None)` - define address `start` as the start of a function, optionally with name `name`
- `SetComment(addr, text)` - place the comment `text` at address `addr`
- `SetFunctionType(addr, sig)` - parse the C++ function signature in `sig` and apply it to the function at address `addr`
- `SetHeaderComment(addr, text)` - place the header/plate/pre-item comment `text` at address `addr`
- `SetName(addr, name)` - set the symbol (or label or name) of address `addr` to `name`
- `SetType(addr, type)` - define address `addr` to be an item of the C++ type `type`
Refer to the source code of `IDA.py` and `Ghidra.py` for examples.
If you would like to add support for a new target application, create a new Python script in `Outputs/ScriptResources/Targets` with the nane `<target-name-without-whitespace>.py` and implement the *BaseDisassemblerInterface* class.
If you also want to have an updating status display, you also need to implement the *BaseStatusHandler* class.
Your target implementation also needs to dispatch analysis at the end, by constructing a *ScriptContext* instance with your disassembler and status implementations, then calling the `process` methon it.
For a simple version of this you can view the *IDA* and *Ghidra* targets, and the *BinaryNinja* one for a more specific analysis dispatch.
When you add a new target and re-compile Il2CppInspector:
@@ -815,6 +812,7 @@ The following books and documents were also very helpful:
- [ARM Architecture Reference Manual ARMv8-A](https://developer.arm.com/docs/ddi0487/latest)
- [Intel 64 and IA-32 Architectures Software Developer's Manual](https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf)
- [Ghidra API documentation](https://ghidra.re/ghidra_docs/api/)
- [Binary Ninja API documentation](https://docs.binary.ninja/dev)
Pizza spinner animation in the GUI made by Chris Gannon - https://gannon.tv/