Implement new GUI and CLI, fix misc. smaller issues (#22)

* Initial commit of new UI c# component

* Initial commit of new UI frontend component

* target WinExe to hide console window in release mode, move ui exe into resources

* force single file publishing and add initial gh workflow for publishing ui

* fix workflow errors

* update dependencies and remove cxxdemangler, as it was outdated

* fix c# single file output due to invalid output path

* smaller tweaks, hack around loops in cpp type layouting

* process other queued exports even if one fails and show error message

* add basic support for processing LC_DYLD_CHAINED_FIXUPS

* ELF loading should not use the file offset for loading the dynamic section

* fix symbol table loading in some modified elfs

* add "start export" button on format selection screen, clear all toasts after selecting an export format

* embed ui executable directly into c# assembly

* only build tauri component in c# release builds

* add il2cpp file (binary, metadata) export to advanced tab

* fix and enable binary ninja fake string segment support

* add support for metadata

* unify logic for getting element type index

* fix new ui not allowing script exports other than ida

* new ui: clear out loaded binary if no IL2CPP images could be loaded

* fix toAddr calls in ghidra script target

* remove dependency on a section being named .text in loaded pe files

* tweak symbol reading a bit and remove sht relocation reading

* add initial support for required forward references in il2cpp types, also fix issues with type names clashing with il2cpp api types

* reduce clang errors for header file, fix better array size struct, emit required forward definitions in header

* expose forward definitions in AppModel, fix issue with method-only used types not being emitted

* remove debug log line

* fix spelling mistakes in gui outputs

* fix il2cpp_array_size_t not being an actual type for later method definitions

* change the default port for new ui dev to 5000

* show current version and hash in new ui footer

* seperate redux ui impl into FrontendCore project

* make inspector version a server api, split up output subtypes and tweak some option names

* add redux CLI based on redux GUI output formats

* replace all Console.WriteLine calls in core inspector with AnsiConsole calls

* add workflow for new cli and add back old gui workflow

* disable aot publish and enable single file for redux cli
This commit is contained in:
Luke
2025-08-15 21:13:32 +02:00
committed by GitHub
parent e161e0f226
commit 3439ca912b
184 changed files with 13425 additions and 964 deletions

View File

@@ -4,6 +4,7 @@
All rights reserved.
*/
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.IO;
@@ -51,7 +52,7 @@ namespace Il2CppInspector
public override IFileFormatStream this[uint index] {
get {
Console.WriteLine($"Extracting binary from {binaryFiles[index].FullName}");
AnsiConsole.WriteLine($"Extracting binary from {binaryFiles[index].FullName}");
IFileFormatStream loaded = null;
// ZipArchiveEntry does not support seeking so we have to close and re-open for each possible load format

View File

@@ -4,6 +4,7 @@
All rights reserved.
*/
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.IO;
@@ -51,7 +52,7 @@ namespace Il2CppInspector
public override IFileFormatStream this[uint index] {
get {
Console.WriteLine($"Extracting binary from {binaryFiles[index].FullName}");
AnsiConsole.WriteLine($"Extracting binary from {binaryFiles[index].FullName}");
IFileFormatStream loaded = null;
// ZipArchiveEntry does not support seeking so we have to close and re-open for each possible load format

View File

@@ -5,6 +5,7 @@
All rights reserved.
*/
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -160,7 +161,7 @@ namespace Il2CppInspector
}
catch (Exception ex)
{
Console.WriteLine($"Got exception {ex} while parsing SHT - reverting to PHT");
AnsiConsole.WriteLine($"Got exception {ex} while parsing SHT - reverting to PHT");
preferPHT = true;
SHT = [];
}
@@ -170,12 +171,12 @@ namespace Il2CppInspector
// These can happen as a result of conversions from other formats to ELF,
// or if the SHT has been deliberately stripped
if (!SHT.Any()) {
Console.WriteLine("ELF binary has no SHT - reverting to PHT");
AnsiConsole.WriteLine("ELF binary has no SHT - reverting to PHT");
preferPHT = true;
}
else if (SHT.All(s => conv.ULong(s.sh_addr) == 0ul)) {
Console.WriteLine("ELF binary SHT is all-zero - reverting to PHT");
AnsiConsole.WriteLine("ELF binary SHT is all-zero - reverting to PHT");
preferPHT = true;
}
@@ -192,7 +193,7 @@ namespace Il2CppInspector
// If the first file offset of the first PHT is zero, assume a dumped image
if (PHT.Any(t => conv.ULong(t.p_vaddr) == 0ul)) {
Console.WriteLine("ELF binary appears to be a dumped memory image");
AnsiConsole.WriteLine("ELF binary appears to be a dumped memory image");
isMemoryImage = true;
}
preferPHT = true;
@@ -202,7 +203,7 @@ namespace Il2CppInspector
else {
var shtOverlap = shtShouldBeOrdered.Aggregate((x, y) => x <= y? y : ulong.MaxValue) == ulong.MaxValue;
if (shtOverlap) {
Console.WriteLine("ELF binary SHT contains invalid ranges - reverting to PHT");
AnsiConsole.WriteLine("ELF binary SHT contains invalid ranges - reverting to PHT");
preferPHT = true;
}
}
@@ -223,7 +224,20 @@ namespace Il2CppInspector
// Get dynamic table if it exists (must be done after rebasing)
if (GetProgramHeader(Elf.PT_DYNAMIC) is TPHdr PT_DYNAMIC)
DynamicTable = ReadArray<elf_dynamic<TWord>>(conv.Long(PT_DYNAMIC.p_offset), (int) (conv.Long(PT_DYNAMIC.p_filesz) / Sizeof(typeof(elf_dynamic<TWord>))));
{
// Important: do not use p_offset here!
// Only load sections should be loaded, which should also include the memory region that contains the dynamic section.
// This just provides the virtual address of the section.
// Some binaries may use the offset here to point to a fake version of the dynamic section,
// making relocation resolution and subsequent analysis fail.
// Reference for Android:
// phdr_table_get_dynamic_section, https://cs.android.com/android/platform/superproject/main/+/main:bionic/linker/linker_phdr.cpp
var dynamicAddr = conv.ULong(PT_DYNAMIC.p_vaddr);
var dynamicSize = (int)(conv.Long(PT_DYNAMIC.p_filesz) / Sizeof(typeof(elf_dynamic<TWord>)));
DynamicTable = ReadMappedArray<elf_dynamic<TWord>>(dynamicAddr, dynamicSize);
}
// Get offset of code section
var codeSegment = PHT.First(x => ((Elf) x.p_flags & Elf.PF_X) == Elf.PF_X);
@@ -254,21 +268,6 @@ namespace Il2CppInspector
StatusUpdate("Finding relocations");
// Two types: add value from offset in image, and add value from specified addend
foreach (var relSection in GetSections(Elf.SHT_REL)) {
reverseMapExclusions.Add(((uint) conv.Int(relSection.sh_offset), (uint) (conv.Int(relSection.sh_offset) + conv.Int(relSection.sh_size) - 1)));
rels.UnionWith(
from rel in ReadArray<elf_rel<TWord>>(conv.Long(relSection.sh_offset), conv.Int(conv.Div(relSection.sh_size, relSection.sh_entsize)))
select new ElfReloc(rel, SHT[relSection.sh_link].sh_offset));
}
foreach (var relaSection in GetSections(Elf.SHT_RELA)) {
reverseMapExclusions.Add(((uint) conv.Int(relaSection.sh_offset), (uint) (conv.Int(relaSection.sh_offset) + conv.Int(relaSection.sh_size) - 1)));
rels.UnionWith(
from rela in ReadArray<elf_rela<TWord>>(conv.Long(relaSection.sh_offset), conv.Int(conv.Div(relaSection.sh_size, relaSection.sh_entsize)))
select new ElfReloc(rela, SHT[relaSection.sh_link].sh_offset));
}
// Relocations in dynamic section
if (GetDynamicEntry(Elf.DT_REL) is elf_dynamic<TWord> dt_rel) {
var dt_rel_count = conv.Int(conv.Div(GetDynamicEntry(Elf.DT_RELSZ).d_un, GetDynamicEntry(Elf.DT_RELENT).d_un));
@@ -291,7 +290,7 @@ namespace Il2CppInspector
}
// Process relocations
var relsz = Sizeof(typeof(TSym));
var relsz = (uint)Sizeof(typeof(TSym));
var currentRel = 0;
var totalRel = rels.Count();
@@ -301,7 +300,26 @@ namespace Il2CppInspector
if (currentRel % 1000 == 0)
StatusUpdate($"Processing relocations ({currentRel * 100 / totalRel:F0}%)");
var symValue = ReadObject<TSym>(conv.Long(rel.SymbolTable) + conv.Long(rel.SymbolIndex) * relsz).st_value; // S
TWord symValue;
try
{
// man this really needs a full overhaul
symValue = ReadMappedObject<TSym>(conv.ULong(rel.SymbolTable) + conv.ULong(rel.SymbolIndex) * relsz)
.st_value; // S
}
catch (InvalidOperationException)
{
try
{
symValue = ReadObject<TSym>(conv.Long(rel.SymbolTable) + conv.Long(rel.SymbolIndex) * relsz)
.st_value; // S
}
catch (InvalidOperationException)
{
continue;
}
}
// Ignore relocations into memory addresses not mapped from the image
try {
@@ -344,7 +362,7 @@ namespace Il2CppInspector
WriteWord(result.newValue);
}
}
Console.WriteLine($"Processed {rels.Count} relocations");
AnsiConsole.WriteLine($"Processed {rels.Count} relocations");
// Build symbol and export tables
processSymbols();
@@ -388,7 +406,8 @@ namespace Il2CppInspector
WriteArray(conv.Long(PT_DYNAMIC.p_offset), dt);
}
private void processSymbols() {
private void processSymbols()
{
StatusUpdate("Processing symbols");
// Three possible symbol tables in ELF files
@@ -436,7 +455,15 @@ namespace Il2CppInspector
symbolTable.Clear();
var exportTable = new Dictionary<string, Export>();
foreach (var pTab in pTables) {
var alreadyProcessed = new List<(TWord offset, TWord count)>();
foreach (var pTab in pTables)
{
if (alreadyProcessed.Any(x =>
conv.ULong(x.offset) == conv.ULong(pTab.offset)))
continue;
alreadyProcessed.Add((pTab.offset, pTab.count));
var symbol_table = ReadArray<TSym>(conv.Long(pTab.offset), conv.Int(pTab.count));
foreach (var symbol in symbol_table)
@@ -463,7 +490,7 @@ namespace Il2CppInspector
var symbolItem = new Symbol {Name = name, Type = type, VirtualAddress = conv.ULong(symbol.st_value) };
symbolTable.TryAdd(name, symbolItem);
if (symbol.st_shndx != (ushort) Elf.SHN_UNDEF)
exportTable.TryAdd(name, new Export {Name = symbolItem.DemangledName, VirtualAddress = conv.ULong(symbol.st_value)});
exportTable.TryAdd(name, new Export {Name = symbolItem.Name, VirtualAddress = conv.ULong(symbol.st_value)});
}
}

View File

@@ -176,7 +176,7 @@ namespace Il2CppInspector
try {
if (type.GetMethod("Load", BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public,
null, new[] { typeof(BinaryObjectStream), typeof(LoadOptions), typeof(EventHandler<string>) }, null)
.Invoke(null, new object[] { binaryObjectStream, loadOptions, statusCallback }) is IFileFormatStream loaded) {
.Invoke(null, [binaryObjectStream, loadOptions, statusCallback]) is IFileFormatStream loaded) {
loaded.IsModified |= preProcessResult.IsStreamModified;
return loaded;

View File

@@ -6,6 +6,7 @@
using System;
using NoisyCowStudios.Bin2Object;
using VersionedSerialization.Attributes;
namespace Il2CppInspector
{
@@ -31,6 +32,7 @@ namespace Il2CppInspector
LC_DYLD_INFO_ONLY = 0x80000022,
LC_FUNCTION_STARTS = 0x26,
LC_ENCRYPTION_INFO_64 = 0x2C,
LC_DYLD_CHAINED_FIXUPS = 0x80000034,
CPU_TYPE_X86 = 7,
CPU_TYPE_X86_64 = 0x01000000 + CPU_TYPE_X86,
@@ -172,4 +174,47 @@ namespace Il2CppInspector
public bool r_extern => ((r_data >> 27) & 1) == 1;
public uint r_type => r_data >> 28;
}
[VersionedStruct]
public partial struct MachODyldChainedFixupsHeader
{
public uint FixupsVersion;
public uint StartsOffset;
public uint ImportsOffset;
public uint SymbolsOffset;
public uint ImportsCount;
public uint ImportsFormat;
public uint SymbolsFormat;
}
[VersionedStruct]
public partial struct MachODyldChainedStartsInSegment
{
public const ushort DYLD_CHAINED_PTR_START_NONE = 0xffff;
public uint StructSize;
public ushort PageSize;
public ushort PointerFormat;
public ulong SegmentOffset;
public uint MaxValidPointer;
public ushort PageCount;
}
public enum MachODyldChainedPtr
{
DYLD_CHAINED_PTR_64 = 2,
DYLD_CHAINED_PTR_64_OFFSET = 6,
}
[VersionedStruct]
public partial struct MachODyldChainedPtr64Rebase
{
private ulong _value;
public ulong Target => _value & 0xfffffffff;
public ulong High8 => (_value >> 36) & 0xff;
public ulong Reserved => (_value >> (36 + 8)) & 0x7f;
public ulong Next => (_value >> (36 + 8 + 7)) & 0xfff;
public bool Bind => ((_value >> (36 + 8 + 7 + 12)) & 0x1) == 0x1;
}
}

View File

@@ -4,12 +4,13 @@
All rights reserved.
*/
using NoisyCowStudios.Bin2Object;
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
@@ -172,15 +173,21 @@ namespace Il2CppInspector
if (encryptionInfo.CryptID != 0)
throw new NotImplementedException("This Mach-O executable is encrypted with FairPlay DRM and cannot be processed. Please provide a decrypted version of the executable.");
break;
case MachO.LC_DYLD_CHAINED_FIXUPS:
var chainedFixupsInfo = ReadObject<MachOLinkEditDataCommand>();
ApplyChainedFixups(chainedFixupsInfo);
break;
}
// There might be other data after the load command so always use the specified total size to step forwards
Position = startPos + loadCommand.Size;
}
// Note: Some binaries do not have __mod_init_func, but instead just __init_offset with offsets to the init functions. This check is disabled.
// Must find __mod_init_func
if (funcTab == null)
return false;
//if (funcTab == null)
// return false;
// Process relocations
foreach (var section in machoSections) {
@@ -188,7 +195,7 @@ namespace Il2CppInspector
// TODO: Implement Mach-O relocations
if (rels.Any()) {
Console.WriteLine("Mach-O file contains relocations (feature not yet implemented)");
AnsiConsole.WriteLine("Mach-O file contains relocations (feature not yet implemented)");
break;
}
}
@@ -282,7 +289,7 @@ namespace Il2CppInspector
: SymbolType.Unknown;
if (type == SymbolType.Unknown) {
Console.WriteLine($"Unknown symbol type: {((int) ntype):x2} {value:x16} " + CxxDemangler.CxxDemangler.Demangle(name));
AnsiConsole.WriteLine($"Unknown symbol type: {((int) ntype):x2} {value:x16} {name}");
}
// Ignore duplicates
@@ -290,7 +297,82 @@ namespace Il2CppInspector
}
}
public override uint[] GetFunctionTable() => ReadArray<TWord>(funcTab.ImageOffset, conv.Int(funcTab.Size) / (Bits / 8)).Select(x => MapVATR(conv.ULong(x)) & 0xffff_fffe).ToArray();
private void ApplyChainedFixups(in MachOLinkEditDataCommand info)
{
var chainedFixupsHeader = ReadVersionedObject<MachODyldChainedFixupsHeader>(info.Offset);
if (chainedFixupsHeader.FixupsVersion != 0)
{
AnsiConsole.WriteLine($"Unsupported chained fixups version: {chainedFixupsHeader.FixupsVersion}");
return;
}
if (chainedFixupsHeader.ImportsFormat != 1 /* DYLD_CHAINED_IMPORT */)
{
AnsiConsole.WriteLine($"Unsupported chained fixups import format: {chainedFixupsHeader.ImportsFormat}");
return;
}
//var importsBase = info.Offset + chainedFixupsHeader.ImportsOffset;
//var imports = ReadPrimitiveArray<uint>(importsBase,
// chainedFixupsHeader.ImportsCount);
//var symbolsBase = info.Offset + chainedFixupsHeader.SymbolsOffset; // todo: apparently this supports zlib
var startsBase = info.Offset + chainedFixupsHeader.StartsOffset;
var segmentCount = ReadPrimitive<uint>(startsBase);
var segmentStartOffsets = ReadPrimitiveArray<uint>(startsBase + 4, segmentCount);
foreach (var startOffset in segmentStartOffsets)
{
if (startOffset == 0)
continue;
var startsInfo = ReadVersionedObject<MachODyldChainedStartsInSegment>(startsBase + startOffset);
if (startsInfo.SegmentOffset == 0)
continue;
var pointerFormat = (MachODyldChainedPtr)startsInfo.PointerFormat;
var pages = ReadPrimitiveArray<ushort>(
startsBase + startOffset + MachODyldChainedStartsInSegment.Size(), startsInfo.PageCount);
for (var i = 0; i < pages.Length; i++)
{
var page = pages[i];
if (page == MachODyldChainedStartsInSegment.DYLD_CHAINED_PTR_START_NONE)
continue;
var chainOffset = startsInfo.SegmentOffset + (ulong)(i * startsInfo.PageSize) + page;
while (true)
{
var currentEntry = ReadVersionedObject<MachODyldChainedPtr64Rebase>((long)chainOffset);
var fixedValue = 0ul;
if (!currentEntry.Bind) // todo: bind
{
fixedValue = pointerFormat switch
{
MachODyldChainedPtr.DYLD_CHAINED_PTR_64
or MachODyldChainedPtr.DYLD_CHAINED_PTR_64_OFFSET
=> currentEntry.High8 << 56 | currentEntry.Target,
_ => fixedValue
};
Write((long)chainOffset, fixedValue);
}
if (currentEntry.Next == 0)
break;
chainOffset += currentEntry.Next * 4;
}
}
}
}
public override uint[] GetFunctionTable() => funcTab == null ? [] : ReadArray<TWord>(funcTab.ImageOffset, conv.Int(funcTab.Size) / (Bits / 8)).Select(x => MapVATR(conv.ULong(x)) & 0xffff_fffe).ToArray();
public override Dictionary<string, Symbol> GetSymbolTable() => symbolTable;

View File

@@ -4,13 +4,14 @@
All rights reserved.
*/
using NoisyCowStudios.Bin2Object;
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
@@ -102,7 +103,7 @@ namespace Il2CppInspector
// Unpacking must be done starting here, one byte after the end of the headers
// Packed or previously packed with Themida? This is purely for information
if (sections.FirstOrDefault(x => x.Name == ".themida") is PESection _)
Console.WriteLine("Themida protection detected");
AnsiConsole.WriteLine("Themida protection detected");
// Packed with anything (including Themida)?
mightBePacked = sections.FirstOrDefault(x => x.Name == ".rdata") is null;
@@ -114,26 +115,31 @@ namespace Il2CppInspector
section.Name = wantedSectionTypes[section.Characteristics];
// Get base of code
GlobalOffset = pe.ImageBase + pe.BaseOfCode - sections.First(x => x.Name == ".text").PointerToRawData;
GlobalOffset = pe.ImageBase + pe.BaseOfCode - sections
.FirstOrDefault(x => x.Characteristics.HasFlag(PE.IMAGE_SCN_MEM_EXECUTE))?.PointerToRawData ?? 0;
// Confirm that .rdata section begins at same place as IAT
var rData = sections.First(x => x.Name == ".rdata");
mightBePacked |= rData.VirtualAddress != IATStart;
var rData = sections.FirstOrDefault(x => x.Name == ".rdata");
mightBePacked |= rData == null || rData.VirtualAddress != IATStart;
if (rData != null)
{
// Calculate start of function pointer table
pFuncTable = rData.PointerToRawData + IATSize;
// Calculate start of function pointer table
pFuncTable = rData.PointerToRawData + IATSize;
// Skip over __guard_check_icall_fptr and __guard_dispatch_icall_fptr if present, then the following zero offset
Position = pFuncTable;
if (pe is PEOptHeader32) {
while (ReadUInt32() != 0)
// Skip over __guard_check_icall_fptr and __guard_dispatch_icall_fptr if present, then the following zero offset
Position = pFuncTable;
if (pe is PEOptHeader32)
{
while (ReadUInt32() != 0)
pFuncTable += 4;
pFuncTable += 4;
pFuncTable += 4;
}
else {
while (ReadUInt64() != 0)
}
else
{
while (ReadUInt64() != 0)
pFuncTable += 8;
pFuncTable += 8;
pFuncTable += 8;
}
}
// In the fist go round, we signal that this is at least a valid PE file; we don't try to unpack yet

View File

@@ -4,11 +4,12 @@
All rights reserved.
*/
using NoisyCowStudios.Bin2Object;
using Spectre.Console;
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
@@ -100,7 +101,7 @@ namespace Il2CppInspector
// Get the entire remaining chunk, or to the end of the file if it doesn't contain the end of the chunk
var length = (uint) Math.Min(chunk.Memory.End - memoryNext, source.Length);
Console.WriteLine($"Writing {length:x8} bytes from {Path.GetFileName(file.Name)} +{fileStart:x8} ({memoryNext:x8}) to target {il2cpp.Position:x8}");
AnsiConsole.WriteLine($"Writing {length:x8} bytes from {Path.GetFileName(file.Name)} +{fileStart:x8} ({memoryNext:x8}) to target {il2cpp.Position:x8}");
// Can't use Stream.CopyTo as it doesn't support length parameter
var buffer = new byte[length];

View File

@@ -20,7 +20,5 @@ namespace Il2CppInspector
public ulong VirtualAddress { get; set; }
public string Name { get; set; }
public SymbolType Type { get; set; }
public string DemangledName => CxxDemangler.CxxDemangler.Demangle(Name);
}
}