IL2CPP: Change metadata and binary to derive from BinaryObjectStream

This commit is contained in:
Katy Coe
2020-12-21 06:37:29 +01:00
parent 620d985b71
commit c00b474f33
31 changed files with 172 additions and 179 deletions

View File

@@ -0,0 +1,73 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace Il2CppInspector
{
// This is a wrapper for multiple binary files of different architectures within a single AAB
internal class AABReader : FileFormatStream<AABReader>
{
private ZipArchive zip;
private ZipArchiveEntry[] binaryFiles;
public override string DefaultFilename => "Package.aab";
protected override bool Init() {
// Check if it's a zip file first because ZipFile.OpenRead is extremely slow if it isn't
// 0x04034B50 = magic file header
// 0x02014B50 = central directory file header (will appear if we merged a split AAB in memory)
var magic = ReadUInt32();
if (magic != 0x04034B50 && magic != 0x02014B50)
return false;
try {
zip = new ZipArchive(this);
// Get list of binary files
binaryFiles = zip.Entries.Where(f => f.FullName.StartsWith("base/lib/") && f.Name == "libil2cpp.so").ToArray();
// This package doesn't contain an IL2CPP binary
if (!binaryFiles.Any())
return false;
}
// Not an archive
catch (InvalidDataException) {
return false;
}
NumImages = (uint) binaryFiles.Count();
return true;
}
public override IFileFormatStream this[uint index] {
get {
Console.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
var binary = binaryFiles[index].Open();
loaded = ElfReader32.Load(binary, LoadOptions, OnStatusUpdate);
binary.Close();
if (loaded != null)
return loaded;
binary = binaryFiles[index].Open();
loaded = ElfReader64.Load(binary, LoadOptions, OnStatusUpdate);
binary.Close();
return loaded;
}
}
}
}

View File

@@ -0,0 +1,73 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace Il2CppInspector
{
// This is a wrapper for multiple binary files of different architectures within a single APK
internal class APKReader : FileFormatStream<APKReader>
{
private ZipArchive zip;
private ZipArchiveEntry[] binaryFiles;
public override string DefaultFilename => "Package.apk";
protected override bool Init() {
// Check if it's a zip file first because ZipFile.OpenRead is extremely slow if it isn't
// 0x04034B50 = magic file header
// 0x02014B50 = central directory file header (will appear if we merged a split APK in memory)
var magic = ReadUInt32();
if (magic != 0x04034B50 && magic != 0x02014B50)
return false;
try {
zip = new ZipArchive(this);
// Get list of binary files
binaryFiles = zip.Entries.Where(f => f.FullName.StartsWith("lib/") && f.Name == "libil2cpp.so").ToArray();
// This package doesn't contain an IL2CPP binary
if (!binaryFiles.Any())
return false;
}
// Not an archive
catch (InvalidDataException) {
return false;
}
NumImages = (uint) binaryFiles.Count();
return true;
}
public override IFileFormatStream this[uint index] {
get {
Console.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
var binary = binaryFiles[index].Open();
loaded = ElfReader32.Load(binary, LoadOptions, OnStatusUpdate);
binary.Close();
if (loaded != null)
return loaded;
binary = binaryFiles[index].Open();
loaded = ElfReader64.Load(binary, LoadOptions, OnStatusUpdate);
binary.Close();
return loaded;
}
}
}
}

View File

@@ -0,0 +1,738 @@
/*
Copyright 2017 Perfare - https://github.com/Perfare/Il2CppDumper
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
internal class ElfReader32 : ElfReader<uint, elf_32_phdr, elf_32_sym, ElfReader32, Convert32>
{
public ElfReader32() : base() {
ElfReloc.GetRelocType = info => (Elf) (info & 0xff);
ElfReloc.GetSymbolIndex = info => info >> 8;
}
public override int Bits => 32;
protected override Elf ArchClass => Elf.ELFCLASS32;
protected override void WriteWord(uint value) => Write(value);
}
internal class ElfReader64 : ElfReader<ulong, elf_64_phdr, elf_64_sym, ElfReader64, Convert64>
{
public ElfReader64() : base() {
ElfReloc.GetRelocType = info => (Elf) (info & 0xffff_ffff);
ElfReloc.GetSymbolIndex = info => info >> 32;
}
public override int Bits => 64;
protected override Elf ArchClass => Elf.ELFCLASS64;
protected override void WriteWord(ulong value) => Write(value);
}
interface IElfReader
{
uint GetPLTAddress();
}
internal abstract class ElfReader<TWord, TPHdr, TSym, TReader, TConvert> : FileFormatStream<TReader>, IElfReader
where TWord : struct
where TPHdr : Ielf_phdr<TWord>, new()
where TSym : Ielf_sym<TWord>, new()
where TConvert : IWordConverter<TWord>, new()
where TReader : FileFormatStream<TReader>
{
private readonly TConvert conv = new TConvert();
// Internal relocation entry helper
protected class ElfReloc
{
public Elf Type;
public TWord Offset;
public TWord? Addend;
public TWord SymbolTable;
public TWord SymbolIndex;
// Equality based on target address
public override bool Equals(object obj) => obj is ElfReloc reloc && Equals(reloc);
public bool Equals(ElfReloc other) {
return Offset.Equals(other.Offset);
}
public override int GetHashCode() => Offset.GetHashCode();
// Cast operators (makes the below code MUCH easier to read)
public ElfReloc(elf_rel<TWord> rel, TWord symbolTable) {
Offset = rel.r_offset;
Addend = null;
Type = GetRelocType(rel.r_info);
SymbolIndex = GetSymbolIndex(rel.r_info);
SymbolTable = symbolTable;
}
public ElfReloc(elf_rela<TWord> rela, TWord symbolTable)
: this(new elf_rel<TWord> { r_info = rela.r_info, r_offset = rela.r_offset }, symbolTable) =>
Addend = rela.r_addend;
public static Func<TWord, Elf> GetRelocType;
public static Func<TWord, TWord> GetSymbolIndex;
}
// See also: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/sizeof
private int Sizeof(Type type) {
int size = 0;
foreach (var i in type.GetTypeInfo().GetFields()) {
if (i.FieldType == typeof(byte) || i.FieldType == typeof(sbyte))
size += sizeof(byte);
if (i.FieldType == typeof(long) || i.FieldType == typeof(ulong))
size += sizeof(ulong);
if (i.FieldType == typeof(int) || i.FieldType == typeof(uint))
size += sizeof(uint);
if (i.FieldType == typeof(short) || i.FieldType == typeof(ushort))
size += sizeof(ushort);
}
return size;
}
private TPHdr[] program_header_table;
private elf_shdr<TWord>[] section_header_table;
private elf_dynamic<TWord>[] dynamic_table;
private elf_header<TWord> elf_header;
private Dictionary<string, elf_shdr<TWord>> sectionByName = new Dictionary<string, elf_shdr<TWord>>();
private List<(uint Start, uint End)> reverseMapExclusions = new List<(uint Start, uint End)>();
private bool preferPHT = false;
private bool isMemoryImage = false;
public override string DefaultFilename => "libil2cpp.so";
public override string Format => Bits == 32 ? "ELF" : "ELF64";
public override string Arch => (Elf) elf_header.e_machine switch {
Elf.EM_386 => "x86",
Elf.EM_ARM => "ARM",
Elf.EM_X86_64 => "x64",
Elf.EM_AARCH64 => "ARM64",
_ => "Unsupported"
};
public override int Bits => (elf_header.m_arch == (uint) Elf.ELFCLASS64) ? 64 : 32;
private elf_shdr<TWord> getSection(Elf sectionIndex) => section_header_table.FirstOrDefault(x => x.sh_type == (uint) sectionIndex);
private IEnumerable<elf_shdr<TWord>> getSections(Elf sectionIndex) => section_header_table.Where(x => x.sh_type == (uint) sectionIndex);
private TPHdr getProgramHeader(Elf programIndex) => program_header_table.FirstOrDefault(x => x.p_type == (uint) programIndex);
private elf_dynamic<TWord> getDynamic(Elf dynamicIndex) => dynamic_table?.FirstOrDefault(x => (Elf) conv.ULong(x.d_tag) == dynamicIndex);
private Dictionary<string, Symbol> symbolTable = new Dictionary<string, Symbol>();
private List<Export> exports = new List<Export>();
protected abstract Elf ArchClass { get; }
protected abstract void WriteWord(TWord value);
protected override bool Init() {
elf_header = ReadObject<elf_header<TWord>>();
// Check for magic bytes
if ((Elf) elf_header.m_dwFormat != Elf.ELFMAG)
return false;
// Ensure supported architecture
if ((Elf) elf_header.m_arch != ArchClass)
return false;
// Get PHT and SHT
program_header_table = ReadArray<TPHdr>(conv.Long(elf_header.e_phoff), elf_header.e_phnum);
section_header_table = ReadArray<elf_shdr<TWord>>(conv.Long(elf_header.e_shoff), elf_header.e_shnum);
// Determine if SHT is valid
// These can happen as a result of conversions from other formats to ELF,
// or if the SHT has been deliberately stripped
if (!section_header_table.Any()) {
Console.WriteLine("ELF binary has no SHT - reverting to PHT");
preferPHT = true;
}
else if (section_header_table.All(s => conv.ULong(s.sh_addr) == 0ul)) {
Console.WriteLine("ELF binary SHT is all-zero - reverting to PHT");
preferPHT = true;
}
// Check for overlaps in sections that are memory-allocated on load
else {
var shtShouldBeOrdered = section_header_table
.Where(s => ((Elf) conv.Int(s.sh_flags) & Elf.SHF_ALLOC) == Elf.SHF_ALLOC)
.OrderBy(s => s.sh_addr)
.Select(s => new[] { conv.ULong(s.sh_addr), conv.ULong(s.sh_addr) + conv.ULong(s.sh_size) })
.SelectMany(s => s);
// No sections that map into memory - this is probably a dumped image
if (!shtShouldBeOrdered.Any()) {
// If the first file offset of the first PHT is zero, assume a dumped image
if (program_header_table.Any(t => conv.ULong(t.p_vaddr) == 0ul)) {
Console.WriteLine("ELF binary appears to be a dumped memory image");
isMemoryImage = true;
}
preferPHT = true;
}
// Sections overlap - this can happen if the ELF has been improperly generated or processed by another tool
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");
preferPHT = true;
}
}
}
// Dumped images must be rebased
if (isMemoryImage) {
if (LoadOptions.ImageBase == 0xffffffff_ffffffff)
throw new InvalidOperationException("To load a dumped ELF image, you must specify the image base virtual address");
rebase(conv.FromULong(LoadOptions.ImageBase));
}
// Get dynamic table if it exists (must be done after rebasing)
if (getProgramHeader(Elf.PT_DYNAMIC) is TPHdr PT_DYNAMIC)
dynamic_table = ReadArray<elf_dynamic<TWord>>(conv.Long(PT_DYNAMIC.p_offset), (int) (conv.Long(PT_DYNAMIC.p_filesz) / Sizeof(typeof(elf_dynamic<TWord>))));
// Get offset of code section
var codeSegment = program_header_table.First(x => ((Elf) x.p_flags & Elf.PF_X) == Elf.PF_X);
GlobalOffset = conv.ULong(conv.Sub(codeSegment.p_vaddr, codeSegment.p_offset));
// Nothing more to do if the image is a memory dump (no section names, relocations or decryption)
if (isMemoryImage)
return true;
// Get section name mappings if there are any
// This is currently only used to defeat the XOR obfuscation handled below
// Note: There can be more than one section with the same name, or unnamed; we take the first section with a given name
if (elf_header.e_shtrndx < section_header_table.Length) {
var pStrtab = section_header_table[elf_header.e_shtrndx].sh_offset;
foreach (var section in section_header_table) {
try {
var name = ReadNullTerminatedString(conv.Long(pStrtab) + section.sh_name);
sectionByName.TryAdd(name, section);
} catch (ArgumentOutOfRangeException) {
// Names have been stripped, maybe previously dumped image
break;
}
}
}
// Find all relocations; target address => (rela header (rels are converted to rela), symbol table base address, is rela?)
var rels = new HashSet<ElfReloc>();
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, section_header_table[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, section_header_table[relaSection.sh_link].sh_offset));
}
// Relocations in dynamic section
if (getDynamic(Elf.DT_REL) is elf_dynamic<TWord> dt_rel) {
var dt_rel_count = conv.Int(conv.Div(getDynamic(Elf.DT_RELSZ).d_un, getDynamic(Elf.DT_RELENT).d_un));
var dt_item_size = Sizeof(typeof(elf_rel<TWord>));
var dt_start = MapVATR(conv.ULong(dt_rel.d_un));
var dt_rel_list = ReadArray<elf_rel<TWord>>(dt_start, dt_rel_count);
var dt_symtab = getDynamic(Elf.DT_SYMTAB).d_un;
reverseMapExclusions.Add((dt_start, (uint) (dt_start + dt_rel_count * dt_item_size - 1)));
rels.UnionWith(from rel in dt_rel_list select new ElfReloc(rel, dt_symtab));
}
if (getDynamic(Elf.DT_RELA) is elf_dynamic<TWord> dt_rela) {
var dt_rela_count = conv.Int(conv.Div(getDynamic(Elf.DT_RELASZ).d_un, getDynamic(Elf.DT_RELAENT).d_un));
var dt_item_size = Sizeof(typeof(elf_rela<TWord>));
var dt_start = MapVATR(conv.ULong(dt_rela.d_un));
var dt_rela_list = ReadArray<elf_rela<TWord>>(dt_start, dt_rela_count);
var dt_symtab = getDynamic(Elf.DT_SYMTAB).d_un;
reverseMapExclusions.Add((dt_start, (uint) (dt_start + dt_rela_count * dt_item_size - 1)));
rels.UnionWith(from rela in dt_rela_list select new ElfReloc(rela, dt_symtab));
}
// Process relocations
var relsz = Sizeof(typeof(TSym));
var currentRel = 0;
var totalRel = rels.Count();
foreach (var rel in rels) {
currentRel++;
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
// Ignore relocations into memory addresses not mapped from the image
try {
Position = MapVATR(conv.ULong(rel.Offset));
}
catch (InvalidOperationException) {
continue;
}
// The addend is specified in the struct for rela, and comes from the target location for rel
var addend = rel.Addend ?? ReadObject<TWord>(); // A
// Only handle relocation types we understand, skip the rest
// Relocation types from https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-54839.html#scrolltoc
// and https://studfiles.net/preview/429210/page:18/
// and http://infocenter.arm.com/help/topic/com.arm.doc.ihi0056b/IHI0056B_aaelf64.pdf (AArch64)
(TWord newValue, bool recognized) result = (rel.Type, (Elf) elf_header.e_machine) switch {
(Elf.R_ARM_ABS32, Elf.EM_ARM) => (conv.Add(symValue, addend), true), // S + A
(Elf.R_ARM_REL32, Elf.EM_ARM) => (conv.Add(conv.Sub(symValue, rel.Offset), addend), true), // S - P + A
(Elf.R_ARM_COPY, Elf.EM_ARM) => (symValue, true), // S
(Elf.R_AARCH64_ABS64, Elf.EM_AARCH64) => (conv.Add(symValue, addend), true), // S + A
(Elf.R_AARCH64_PREL64, Elf.EM_AARCH64) => (conv.Sub(conv.Add(symValue, addend), rel.Offset), true), // S + A - P
(Elf.R_AARCH64_GLOB_DAT, Elf.EM_AARCH64) => (conv.Add(symValue, addend), true), // S + A
(Elf.R_AARCH64_JUMP_SLOT, Elf.EM_AARCH64) => (conv.Add(symValue, addend), true), // S + A
(Elf.R_AARCH64_RELATIVE, Elf.EM_AARCH64) => (conv.Add(symValue, addend), true), // Delta(S) + A
(Elf.R_386_32, Elf.EM_386) => (conv.Add(symValue, addend), true), // S + A
(Elf.R_386_PC32, Elf.EM_386) => (conv.Sub(conv.Add(symValue, addend), rel.Offset), true), // S + A - P
(Elf.R_386_GLOB_DAT, Elf.EM_386) => (symValue, true), // S
(Elf.R_386_JMP_SLOT, Elf.EM_386) => (symValue, true), // S
(Elf.R_AMD64_64, Elf.EM_AARCH64) => (conv.Add(symValue, addend), true), // S + A
_ => (default(TWord), false)
};
if (result.recognized) {
Position = MapVATR(conv.ULong(rel.Offset));
WriteWord(result.newValue);
}
}
Console.WriteLine($"Processed {rels.Count} relocations");
// Detect and defeat various kinds of XOR encryption
StatusUpdate("Detecting encryption");
if (getDynamic(Elf.DT_INIT) != null && sectionByName.ContainsKey(".rodata")) {
// Use the data section to determine some possible keys
// If the data section uses striped encryption, bucketing the whole section will not give the correct key
var roDataBytes = ReadBytes(conv.Long(sectionByName[".rodata"].sh_offset), conv.Int(sectionByName[".rodata"].sh_size));
var xorKeyCandidateStriped = roDataBytes.Take(1024).GroupBy(b => b).OrderByDescending(f => f.Count()).First().Key;
var xorKeyCandidateFull = roDataBytes.GroupBy(b => b).OrderByDescending(f => f.Count()).First().Key;
// Select test nibbles and values for ARM instructions depending on architecture (ARMv7 / AArch64)
var testValues = new Dictionary<int, (int, int, int, int)> {
[32] = (8, 28, 0x0, 0xE),
[64] = (4, 28, 0xE, 0xF)
};
var (armNibbleB, armNibbleT, armValueB, armValueT) = testValues[Bits];
var instructionsToTest = 256;
// This gives us an idea of whether the code might be encrypted
var textFirstDWords = ReadArray<uint>(conv.Long(sectionByName[".text"].sh_offset), instructionsToTest);
var bottom = textFirstDWords.Select(w => (w >> armNibbleB) & 0xF).GroupBy(n => n).OrderByDescending(f => f.Count()).First().Key;
var top = textFirstDWords.Select(w => w >> armNibbleT).GroupBy(n => n).OrderByDescending(f => f.Count()).First().Key;
var xorKeyCandidateFromCode = (byte) (((top ^ armValueT) << 4) | (bottom ^ armValueB));
// If the first part of the data section is encrypted, proceed
if (xorKeyCandidateStriped != 0x00) {
// Some files may use a striped encryption whereby alternate blocks are encrypted and un-encrypted
// The first part of each section is always encrypted.
// We refer to issue #96 where the code uses striped encryption in 4KB blocks
// We perform heuristics for block of size blockSize below
const int blockSize = 0x100;
const int maxBrokenRun = 4;
const int minMultiplierInValid = 6;
const int minTotalValidInBucket = 0x10;
// Take all of the instructions from the code section starting on a VA block boundary and determine which are valid
var startSkip = 0;
if (conv.Int(sectionByName[".text"].sh_addr) % blockSize != 0)
startSkip = blockSize - conv.Int(sectionByName[".text"].sh_addr) % blockSize;
var insts = ReadArray<uint>(conv.Long(sectionByName[".text"].sh_offset) + startSkip, (conv.Int(sectionByName[".text"].sh_size) - startSkip) / 4);
var instsValid = insts.Select(i => Bits == 32? isCommonARMv7(i) : isCommonARMv8A(i)).ToList();
// Use RLE to produce frequency distribution of number of consecutive valid and invalid instructions,
// allowing for maxBrokenRun breaks in valid instructions in a row before considering a run to have ended
var freqValid = new SortedDictionary<int, int>();
var runLength = 0;
var brokenRun = 0;
foreach (var i in instsValid) {
if (i) {
runLength = runLength + brokenRun + 1;
brokenRun = 0;
} else if (runLength > 0) {
brokenRun++;
if (brokenRun > maxBrokenRun) {
if (freqValid.ContainsKey(runLength))
freqValid[runLength]++;
else
freqValid[runLength] = 1;
runLength = 0;
}
}
}
// Create a histogram of how often each range of valid instruction counts occurred
// The uses of 4 refer to the size of an ARM instruction
var histValid = freqValid.GroupBy(f => f.Key - (f.Key % (blockSize / 4)))
.Select(f => new {
Key = f.Key * 4,
Value = f.Sum(x => x.Value)
}).ToDictionary(x => x.Key, x => x.Value);
// Find first point in the histogram where the number of valid instructions suddenly spikes
var stripeSize = (uint) histValid.Zip(histValid.Skip(1), (p,c) => (p,c))
.FirstOrDefault(x => x.c.Value >= x.p.Value * minMultiplierInValid && x.c.Value >= minTotalValidInBucket).c.Key;
// Select the key
// If more than one key candidates are the same, select the most common candidate
var keys = new [] { xorKeyCandidateFromCode, xorKeyCandidateStriped, xorKeyCandidateFull };
var bestKey = keys.GroupBy(k => k).OrderByDescending(k => k.Count()).First();
var xorKey = bestKey.Key;
// Otherwise choose according to striped/full encryption
if (bestKey.Count() == 1) {
xorKey = keys.OrderByDescending(k => textFirstDWords.Select(w => w ^ (k << 24) ^ (k << 16) ^ (k << 8) ^ k)
.Count(w => Bits == 32? isCommonARMv7((uint) w) : isCommonARMv8A((uint) w))).First();
}
StatusUpdate("Decrypting");
Console.WriteLine($"Performing XOR decryption (key: 0x{xorKey:X2}, stripe size: 0x{stripeSize:X4})");
xorSection(".text", xorKey, stripeSize);
xorSection(".rodata", xorKey, stripeSize);
IsModified = true;
}
}
// Detect more sophisticated packing
// We have seen several examples (eg. #14 and #26) where most of the file is zeroed
// and packed data is found in the latter third. So far these files always have zeroed .rodata sections
if (sectionByName.ContainsKey(".rodata")) {
var rodataBytes = ReadBytes(conv.Long(sectionByName[".rodata"].sh_offset), conv.Int(sectionByName[".rodata"].sh_size));
if (rodataBytes.All(b => b == 0x00))
throw new InvalidOperationException("This IL2CPP binary is packed in a way not currently supported by Il2CppInspector and cannot be loaded.");
}
// Build symbol and export tables
processSymbols();
return true;
}
// https://developer.arm.com/documentation/ddi0406/cb/Application-Level-Architecture/ARM-Instruction-Set-Encoding/ARM-instruction-set-encoding
private bool isCommonARMv7(uint inst) {
var cond = inst >> 28; // We'll allow 0x1111 (for BL/BLX), AL, EQ, NE, GE, LT, GT, LE only
if (cond != 0b1111 && cond != 0b1110 && cond != 0b0000 && cond != 0b0001 && cond != 0b1010 && cond != 0b1011 && cond != 0b1100 && cond != 0b1101)
return false;
var op1 = (inst >> 25) & 7;
// Disallow media instructions
var op = (inst >> 4) & 1;
if (op1 == 0b011 && op == 1)
return false;
// Disallow co-processor instructions
if (op1 == 0b110 || op1 == 0b111)
return false;
// Disallow 0b1111 cond except for BL and BLX
if (cond == 0b1111) {
var op1_1 = (inst >> 20) & 0b11111111;
if ((op1_1 >> 5) != 0b101)
return false;
}
// Disallow MSR and other miscellaneous
if (op == 1) {
var op1_1 = (inst >> 20) & 0b11111;
var op2 = (inst >> 4) & 0b1111;
if (op1_1 == 0b10010 || op1_1 == 0b10110 || op1_1 == 0b10000 || op1_1 == 0b10100)
return false;
// Disallow synchronization primitives
if ((op1_1 >> 4) == 1)
return false;
}
// Probably a common instruction
return true;
}
// https://montcs.bloomu.edu/Information/ARMv8/ARMv8-A_Architecture_Reference_Manual_(Issue_A.a).pdf
private bool isCommonARMv8A(uint inst) {
var op = (inst >> 24) & 0b11111;
// Disallow unexpected, SIMD and FP
if ((op >> 3) == 0 || (op >> 1) == 0b0111 || (op >> 1) == 0b1111)
return false;
// Disallow exception generation and system instructions
if ((inst >> 24) == 0b11010100 || (inst >> 22) == 0b1101010100)
return false;
// Disallow bitfield and extract
if (op == 0b10011)
return false;
// Disallow conditional compare and data processing
if ((op >> 1) == 0b1101)
return false;
return true;
}
private void xorRange(int offset, int length, byte xorValue) {
var bytes = ReadBytes(offset, length);
bytes = bytes.Select(b => (byte) (b ^ xorValue)).ToArray();
Write(offset, bytes);
}
private void xorSection(string sectionName, byte xorValue, uint stripeSize) {
var section = sectionByName[sectionName];
// First part up to stripe size boundary is always encrypted, first full block is always encrypted
var start = conv.Int(section.sh_offset);
var length = conv.Int(section.sh_size);
// Non-striped
if (stripeSize == 0) {
xorRange(start, length, xorValue);
return;
}
// Striped
// The first block's length is the distance to the boundary to the first stripe size + one stripe
var firstBlockLength = stripeSize;
if (start % stripeSize != 0)
firstBlockLength += stripeSize - (uint) (start % stripeSize);
xorRange(start, (int) firstBlockLength, xorValue);
// Step forward two stripe sizes at a time, decrypting the first and ignoring the second
for (var pos = start + firstBlockLength + stripeSize; pos < start + length; pos += stripeSize * 2) {
var size = Math.Min(stripeSize, start + length - pos);
xorRange((int) pos, (int) size, xorValue);
}
}
// Rebase the image to a new virtual address
private void rebase(TWord imageBase) {
// Rebase PHT
foreach (var segment in program_header_table) {
segment.p_offset = segment.p_vaddr;
segment.p_vaddr = conv.Add(segment.p_vaddr, imageBase);
segment.p_filesz = segment.p_memsz;
}
// Rewrite to stream
WriteArray(conv.Long(elf_header.e_phoff), program_header_table);
IsModified = true;
// Rebase dynamic table if it exists
// Note we have to rebase the PHT first to get the correct location to read this
if (!(getProgramHeader(Elf.PT_DYNAMIC) is TPHdr PT_DYNAMIC))
return;
var dt = ReadArray<elf_dynamic<TWord>>(conv.Long(PT_DYNAMIC.p_offset), (int) (conv.Long(PT_DYNAMIC.p_filesz) / Sizeof(typeof(elf_dynamic<TWord>))));
// Every table containing virtual address pointers
// https://docs.oracle.com/cd/E19683-01/817-3677/chapter6-42444/index.html
var tablesToRebase = new [] {
Elf.DT_PLTGOT, Elf.DT_HASH, Elf.DT_STRTAB, Elf.DT_SYMTAB, Elf.DT_RELA,
Elf.DT_INIT, Elf.DT_FINI, Elf.DT_REL, Elf.DT_JMPREL, Elf.DT_INIT_ARRAY, Elf.DT_FINI_ARRAY,
Elf.DT_PREINIT_ARRAY, Elf.DT_MOVETAB, Elf.DT_VERDEF, Elf.DT_VERNEED, Elf.DT_SYMINFO
};
// Rebase dynamic tables
foreach (var section in dt.Where(x => tablesToRebase.Contains((Elf) conv.ULong(x.d_tag))))
section.d_un = conv.Add(section.d_un, imageBase);
// Rewrite to stream
WriteArray(conv.Long(PT_DYNAMIC.p_offset), dt);
}
private void processSymbols() {
StatusUpdate("Processing symbols");
// Three possible symbol tables in ELF files
var pTables = new List<(TWord offset, TWord count, TWord strings)>();
// String table (a sequence of null-terminated strings, total length in sh_size
var SHT_STRTAB = getSection(Elf.SHT_STRTAB);
if (SHT_STRTAB != null) {
// Section header shared object symbol table (.symtab)
if (getSection(Elf.SHT_SYMTAB) is elf_shdr<TWord> SHT_SYMTAB)
pTables.Add((SHT_SYMTAB.sh_offset, conv.Div(SHT_SYMTAB.sh_size, SHT_SYMTAB.sh_entsize), SHT_STRTAB.sh_offset));
// Section header executable symbol table (.dynsym)
if (getSection(Elf.SHT_DYNSYM) is elf_shdr<TWord> SHT_DYNSYM)
pTables.Add((SHT_DYNSYM.sh_offset, conv.Div(SHT_DYNSYM.sh_size, SHT_DYNSYM.sh_entsize), SHT_STRTAB.sh_offset));
}
// Symbol table in dynamic section (DT_SYMTAB)
// Normally the same as .dynsym except that .dynsym may be removed in stripped binaries
// Dynamic string table
if (getDynamic(Elf.DT_STRTAB) is elf_dynamic<TWord> DT_STRTAB) {
if (getDynamic(Elf.DT_SYMTAB) is elf_dynamic<TWord> DT_SYMTAB) {
// Find the next pointer in the dynamic table to calculate the length of the symbol table
var end = (from x in dynamic_table where conv.Gt(x.d_un, DT_SYMTAB.d_un) orderby x.d_un select x).First().d_un;
// Dynamic symbol table
pTables.Add((
conv.FromUInt(MapVATR(conv.ULong(DT_SYMTAB.d_un))),
conv.Div(conv.Sub(end, DT_SYMTAB.d_un), Sizeof(typeof(TSym))),
DT_STRTAB.d_un
));
}
}
// Now iterate through all of the symbol and string tables we found to build a full list
symbolTable.Clear();
var exportTable = new Dictionary<string, Export>();
foreach (var pTab in pTables) {
var symbol_table = ReadArray<TSym>(conv.Long(pTab.offset), conv.Int(pTab.count));
foreach (var symbol in symbol_table) {
string name = string.Empty;
try {
name = ReadNullTerminatedString(conv.Long(pTab.strings) + symbol.st_name);
} catch (ArgumentOutOfRangeException) {
// Name has been stripped, maybe previously dumped image
continue;
}
var type = symbol.type == Elf.STT_FUNC? SymbolType.Function
: symbol.type == Elf.STT_OBJECT || symbol.type == Elf.STT_COMMON? SymbolType.Name
: SymbolType.Unknown;
if (symbol.st_shndx == (ushort) Elf.SHN_UNDEF)
type = SymbolType.Import;
// Avoid duplicates
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)});
}
}
exports = exportTable.Values.ToList();
}
public override Dictionary<string, Symbol> GetSymbolTable() => symbolTable;
public override IEnumerable<Export> GetExports() => exports;
public override uint[] GetFunctionTable() {
// INIT_ARRAY contains a list of pointers to initialization functions (not all functions in the binary)
// INIT_ARRAYSZ contains the size of INIT_ARRAY
// INIT_ARRAY is probably broken in dumped images and resaved dumped images
if (getDynamic(Elf.DT_INIT_ARRAY) == null || getDynamic(Elf.DT_INIT_ARRAYSZ) == null || isMemoryImage)
return Array.Empty<uint>();
var init = MapVATR(conv.ULong(getDynamic(Elf.DT_INIT_ARRAY).d_un));
var size = getDynamic(Elf.DT_INIT_ARRAYSZ).d_un;
var init_array = conv.UIntArray(ReadArray<TWord>(init, conv.Int(size) / (Bits / 8)));
// Additionally, check if there is an old-style DT_INIT function and include it in the list if so
if (getDynamic(Elf.DT_INIT) != null)
init_array = init_array.Concat(conv.UIntArray(new[] { getDynamic(Elf.DT_INIT).d_un })).ToArray();
return init_array.Select(x => MapVATR(x)).ToArray();
}
public override IEnumerable<Section> GetSections() {
// If the sections have been stripped, use the segment list from the PHT instead
if (preferPHT)
return program_header_table.Select(p => new Section {
VirtualStart = conv.ULong(p.p_vaddr),
VirtualEnd = conv.ULong(p.p_vaddr) + conv.ULong(p.p_memsz) - 1,
ImageStart = (uint) conv.Int(p.p_offset),
ImageEnd = (uint) conv.Int(p.p_offset) + (uint) conv.Int(p.p_filesz) - 1,
// Not correct but probably the best we can do
IsData = ((Elf) p.p_flags & Elf.PF_R) != 0,
IsExec = ((Elf) p.p_flags & Elf.PF_X) != 0,
IsBSS = conv.Int(p.p_filesz) == 0,
Name = string.Empty
});
// Return sections list
return section_header_table.Select(s => new Section {
VirtualStart = conv.ULong(s.sh_addr),
VirtualEnd = conv.ULong(s.sh_addr) + conv.ULong(s.sh_size) - 1,
ImageStart = (uint) conv.Int(s.sh_offset),
ImageEnd = (uint) conv.Int(s.sh_offset) + (uint) conv.Int(s.sh_size) - 1,
IsData = ((Elf) conv.Int(s.sh_flags) & Elf.SHF_ALLOC) == Elf.SHF_ALLOC && ((Elf) conv.Int(s.sh_flags) & Elf.SHF_EXECINSTR) == 0 && (Elf) s.sh_type == Elf.SHT_PROGBITS,
IsExec = ((Elf) conv.Int(s.sh_flags) & Elf.SHF_EXECINSTR) == Elf.SHF_EXECINSTR && (Elf) s.sh_type == Elf.SHT_PROGBITS,
IsBSS = (Elf) s.sh_type == Elf.SHT_NOBITS,
Name = sectionByName.First(sbn => conv.Int(sbn.Value.sh_offset) == conv.Int(s.sh_offset)).Key
});
}
// Map a virtual address to an offset into the image file. Throws an exception if the virtual address is not mapped into the file.
// Note if uiAddr is a valid segment but filesz < memsz and the adjusted uiAddr falls between the range of filesz and memsz,
// an exception will be thrown. This area of memory is assumed to contain all zeroes.
public override uint MapVATR(ulong uiAddr) {
// Additions in the argument to MapVATR may cause an overflow which should be discarded for 32-bit files
if (Bits == 32)
uiAddr &= 0xffff_ffff;
var program_header_table = this.program_header_table.First(x => uiAddr >= conv.ULong(x.p_vaddr) && uiAddr <= conv.ULong(conv.Add(x.p_vaddr, x.p_filesz)));
return (uint) (uiAddr - conv.ULong(conv.Sub(program_header_table.p_vaddr, program_header_table.p_offset)));
}
public override ulong MapFileOffsetToVA(uint offset) {
// Exclude relocation areas
if (reverseMapExclusions.Any(r => offset >= r.Start && offset <= r.End))
throw new InvalidOperationException("Attempt to map to a relocation address");
var section = program_header_table.First(x => offset >= conv.Int(x.p_offset) && offset < conv.Int(x.p_offset) + conv.Int(x.p_filesz));
return conv.ULong(section.p_vaddr) + offset - conv.ULong(section.p_offset);
}
// Get the address of the procedure linkage table (.got.plt) which is needed for some disassemblies
public uint GetPLTAddress() => (uint) conv.ULong(getDynamic(Elf.DT_PLTGOT).d_un);
}
}

View File

@@ -0,0 +1,16 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
namespace Il2CppInspector
{
// A code file function export
public class Export
{
public int Ordinal;
public string Name;
public ulong VirtualAddress;
}
}

View File

@@ -0,0 +1,302 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
public interface IFileFormatStream
{
double Version { get; set; }
long Length { get; }
uint NumImages { get; }
string DefaultFilename { get; }
bool IsModified { get; }
IEnumerable<IFileFormatStream> Images { get; } // Each child image of this object (eg. 32/64-bit versions in Fat MachO file)
IFileFormatStream this[uint index] { get; } // With no additional override, one object = one file, this[0] == this
long Position { get; set; }
string Format { get; }
string Arch { get; }
int Bits { get; }
ulong GlobalOffset { get; } // The virtual address where the code section (.text) would be loaded in memory
ulong ImageBase { get; } // The virtual address of where the image would be loaded in memory (same as GlobalOffset except for PE)
IEnumerable<IFileFormatStream> TryNextLoadStrategy(); // Some images can be loaded multiple ways, eg. default, packed
Dictionary<string, Symbol> GetSymbolTable();
uint[] GetFunctionTable();
IEnumerable<Export> GetExports();
IEnumerable<Section> GetSections();
bool TryGetSections(out IEnumerable<Section> sections);
uint MapVATR(ulong uiAddr);
bool TryMapVATR(ulong uiAddr, out uint fileOffset);
ulong MapFileOffsetToVA(uint offset);
bool TryMapFileOffsetToVA(uint offset, out ulong va);
byte ReadByte();
byte ReadByte(long uiAddr);
byte[] ReadBytes(int count);
byte[] ReadBytes(long uiAddr, int count);
bool ReadBoolean();
bool ReadBoolean(long uiAddr);
long ReadInt64();
long ReadInt64(long uiAddr);
int ReadInt32();
int ReadInt32(long uiAddr);
short ReadInt16();
short ReadInt16(long uiAddr);
ulong ReadUInt64();
ulong ReadUInt64(long uiAddr);
uint ReadUInt32();
uint ReadUInt32(long uiAddr);
ushort ReadUInt16();
ushort ReadUInt16(long uiAddr);
U ReadObject<U>() where U : new();
U ReadObject<U>(long uiAddr) where U : new();
U[] ReadArray<U>(int count) where U : new();
U[] ReadArray<U>(long uiAddr, int count) where U : new();
string ReadNullTerminatedString(Encoding encoding = null);
string ReadNullTerminatedString(long uiAddr, Encoding encoding = null);
string ReadFixedLengthString(int length, Encoding encoding = null);
string ReadFixedLengthString(long uiAddr, int length, Encoding encoding = null);
long ReadWord();
long ReadWord(long uiAddr);
long[] ReadWordArray(int count);
long[] ReadWordArray(long uiAddr, int count);
byte ReadMappedByte(ulong uiAddr);
byte[] ReadMappedBytes(ulong uiAddr, int count);
bool ReadMappedBoolean(ulong uiAddr);
long ReadMappedInt64(ulong uiAddr);
int ReadMappedInt32(ulong uiAddr);
short ReadMappedInt16(ulong uiAddr);
ulong ReadMappedUInt64(ulong uiAddr);
uint ReadMappedUInt32(ulong uiAddr);
ushort ReadMappedUInt16(ulong uiAddr);
U ReadMappedObject<U>(ulong uiAddr) where U : new();
U[] ReadMappedArray<U>(ulong uiAddr, int count) where U : new();
string ReadMappedNullTerminatedString(ulong uiAddr, Encoding encoding = null);
string ReadMappedFixedLengthString(ulong uiAddr, int length, Encoding encoding = null);
long ReadMappedWord(ulong uiAddr);
long[] ReadMappedWordArray(ulong uiAddr, int count);
List<U> ReadMappedObjectPointerArray<U>(ulong uiAddr, int count) where U : new();
void WriteEndianBytes(byte[] bytes);
void Write(long int64);
void Write(ulong uint64);
void Write(int int32);
void Write(uint uint32);
void Write(short int16);
void Write(ushort uint16);
void Write(long addr, byte[] bytes);
void Write(long addr, long int64);
void Write(long addr, ulong uint64);
void Write(long addr, int int32);
void Write(long addr, uint uint32);
void Write(long addr, short int16);
void Write(long addr, ushort uint16);
void Write(long addr, byte value);
void Write(long addr, bool value);
void WriteObject<T>(long addr, T obj);
void WriteObject<T>(T obj);
void WriteArray<T>(long addr, T[] array);
void WriteArray<T>(T[] array);
void WriteNullTerminatedString(long addr, string str, Encoding encoding = null);
void WriteNullTerminatedString(string str, Encoding encoding = null);
void WriteFixedLengthString(long addr, string str, int size = -1, Encoding encoding = null);
void WriteFixedLengthString(string str, int size = -1, Encoding encoding = null);
EventHandler<string> OnStatusUpdate { get; set; }
public void AddPrimitiveMapping(Type objType, Type streamType);
public void CopyTo(Stream stream);
}
public class FileFormatStream
{
// Helper method to try all defined file formats when the contents of the binary is unknown
public static IFileFormatStream Load(string filename, LoadOptions loadOptions = null, EventHandler<string> statusCallback = null)
=> Load(new FileStream(filename, FileMode.Open, FileAccess.Read), loadOptions, statusCallback);
public static IFileFormatStream Load(Stream stream, LoadOptions loadOptions = null, EventHandler<string> statusCallback = null) {
var types = Assembly.GetExecutingAssembly().DefinedTypes
.Where(x => x.ImplementedInterfaces.Contains(typeof(IFileFormatStream)) && !x.IsGenericTypeDefinition);
foreach (var type in types) {
try {
if (type.GetMethod("Load", BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public,
null, new[] {typeof(Stream), typeof(LoadOptions), typeof(EventHandler<string>)}, null)
.Invoke(null, new object[] {stream, loadOptions, statusCallback}) is IFileFormatStream loaded)
return loaded;
}
catch (TargetInvocationException ex) {
throw ex.InnerException;
}
}
return null;
}
}
public abstract class FileFormatStream<T> : BinaryObjectStream, IFileFormatStream where T : FileFormatStream<T>
{
public abstract string DefaultFilename { get; }
public bool IsModified { get; protected set; } = false;
public uint NumImages { get; protected set; } = 1;
public ulong GlobalOffset { get; protected set; }
public virtual ulong ImageBase => GlobalOffset;
public virtual string Format => throw new NotImplementedException();
public virtual string Arch => throw new NotImplementedException();
public virtual int Bits => throw new NotImplementedException();
// Extra parameters to be passed to a loader
protected LoadOptions LoadOptions;
public EventHandler<string> OnStatusUpdate { get; set; }
protected void StatusUpdate(string status) => OnStatusUpdate?.Invoke(this, status);
public IEnumerable<IFileFormatStream> Images {
get {
for (uint i = 0; i < NumImages; i++)
yield return this[i];
}
}
public static T Load(string filename, LoadOptions loadOptions = null, EventHandler<string> statusCallback = null) {
using var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
return Load(stream, loadOptions, statusCallback);
}
public static T Load(Stream stream, LoadOptions loadOptions = null, EventHandler<string> statusCallback = null) {
// Copy the original stream in case we modify it
if (stream.CanSeek)
stream.Position = 0;
var binary = (T) Activator.CreateInstance(typeof(T));
stream.CopyTo(binary);
binary.Position = 0;
return binary.InitImpl(loadOptions, statusCallback) ? binary : null;
}
private bool InitImpl(LoadOptions loadOptions, EventHandler<string> statusCallback) {
LoadOptions = loadOptions;
OnStatusUpdate = statusCallback;
return Init();
}
// Confirm file is valid and set up RVA mappings
protected virtual bool Init() => throw new NotImplementedException();
// Choose an image within the file for multi-architecture binaries
public virtual IFileFormatStream this[uint index] => (index == 0)? this : throw new IndexOutOfRangeException("Binary image index out of bounds");
// For images that can be loaded and then tested with Il2CppBinary in multiple ways, get the next possible version of the image
public virtual IEnumerable<IFileFormatStream> TryNextLoadStrategy() { yield return this; }
// Find search locations in the symbol table for Il2Cpp data
public virtual Dictionary<string, Symbol> GetSymbolTable() => new Dictionary<string, Symbol>();
// Find search locations in the machine code for Il2Cpp data
public virtual uint[] GetFunctionTable() => throw new NotImplementedException();
// Find all symbol exports for the image
public virtual IEnumerable<Export> GetExports() => null;
// Get all sections for the image in a universal format
public virtual IEnumerable<Section> GetSections() => throw new NotImplementedException();
public bool TryGetSections(out IEnumerable<Section> sections) {
try {
sections = GetSections();
return true;
} catch (NotImplementedException) {
sections = null;
return false;
}
}
// Map an RVA to an offset into the file image
// No mapping by default
public virtual uint MapVATR(ulong uiAddr) => (uint) uiAddr;
// Try to map an RVA to an offset in the file image
public bool TryMapVATR(ulong uiAddr, out uint fileOffset) {
try {
fileOffset = MapVATR(uiAddr);
return true;
} catch (InvalidOperationException) {
fileOffset = 0;
return false;
}
}
// Map an offset into the file image to an RVA
// No mapping by default
public virtual ulong MapFileOffsetToVA(uint offset) => offset;
// Try to map an offset into the file image to an RVA
public bool TryMapFileOffsetToVA(uint offset, out ulong va) {
try {
va = MapFileOffsetToVA(offset);
return true;
}
catch (InvalidOperationException) {
va = 0;
return false;
}
}
// Read a file format dependent word (32 or 64 bits)
// The primitive mappings in Bin2Object will automatically read a uint if the file is 32-bit
public long ReadWord() => ReadObject<long>();
public long ReadWord(long uiAddr) => ReadObject<long>(uiAddr);
public long[] ReadWordArray(int count) => ReadArray<long>(count);
public long[] ReadWordArray(long uiAddr, int count) => ReadArray<long>(uiAddr, count);
// Retrieve items from specified RVA(s)
public byte ReadMappedByte(ulong uiAddr) => ReadByte(MapVATR(uiAddr));
public byte[] ReadMappedBytes(ulong uiAddr, int count) => count > 0? ReadBytes(MapVATR(uiAddr), count) : new byte[0];
public bool ReadMappedBoolean(ulong uiAddr) => ReadBoolean(MapVATR(uiAddr));
public long ReadMappedInt64(ulong uiAddr) => ReadInt64(MapVATR(uiAddr));
public int ReadMappedInt32(ulong uiAddr) => ReadInt32(MapVATR(uiAddr));
public short ReadMappedInt16(ulong uiAddr) => ReadInt16(MapVATR(uiAddr));
public ulong ReadMappedUInt64(ulong uiAddr) => ReadUInt64(MapVATR(uiAddr));
public uint ReadMappedUInt32(ulong uiAddr) => ReadUInt32(MapVATR(uiAddr));
public ushort ReadMappedUInt16(ulong uiAddr) => ReadUInt16(MapVATR(uiAddr));
public U ReadMappedObject<U>(ulong uiAddr) where U : new() => ReadObject<U>(MapVATR(uiAddr));
public U[] ReadMappedArray<U>(ulong uiAddr, int count) where U : new() => count > 0 ? ReadArray<U>(MapVATR(uiAddr), count) : new U[0];
public string ReadMappedNullTerminatedString(ulong uiAddr, Encoding encoding = null) => ReadNullTerminatedString(MapVATR(uiAddr), encoding);
public string ReadMappedFixedLengthString(ulong uiAddr, int length, Encoding encoding = null) => ReadFixedLengthString(MapVATR(uiAddr), length, encoding);
// Read a file format dependent array of words (32 or 64 bits)
// The primitive mappings in Bin2Object will automatically read a uint if the file is 32-bit
public long ReadMappedWord(ulong uiAddr) => ReadWord(MapVATR(uiAddr));
public long[] ReadMappedWordArray(ulong uiAddr, int count) => count > 0 ? ReadArray<long>(MapVATR(uiAddr), count) : new long[0];
// Reads a list of pointers, then reads each object pointed to
public List<U> ReadMappedObjectPointerArray<U>(ulong uiAddr, int count) where U : new() {
var pointers = ReadMappedArray<ulong>(uiAddr, count);
var array = new List<U>();
for (int i = 0; i < count; i++)
array.Add(ReadMappedObject<U>(pointers[i]));
return array;
}
}
}

View File

@@ -0,0 +1,282 @@
/*
Copyright 2017 Perfare - https://github.com/Perfare/Il2CppDumper
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
[Flags]
public enum Elf : uint
{
// elf_header.m_dwFormat
ELFMAG = 0x464c457f, // "\177ELF"
// elf_header.e_machine
EM_386 = 0x03,
EM_ARM = 0x28,
EM_X86_64 = 0x3E,
EM_AARCH64 = 0xB7,
// elf_header.m_arch
ELFCLASS32 = 1,
ELFCLASS64 = 2,
// elf_header.e_Type
ET_EXEC = 2,
// PHTs
PT_LOAD = 1,
PT_DYNAMIC = 2,
PF_X = 1,
PF_W = 2,
PF_R = 4,
// SHTs
SHT_PROGBITS = 1,
SHT_SYMTAB = 2,
SHT_STRTAB = 3,
SHT_RELA = 4,
SHT_NOBITS = 8,
SHT_REL = 9,
SHT_DYNSYM = 11,
// SHNs
SHN_UNDEF = 0,
// STTs
STT_NOTYPE = 0,
STT_OBJECT = 1,
STT_FUNC = 2,
STT_SECTION = 3,
STT_FILE = 4,
STT_COMMON = 5,
STT_LOOS = 10,
STT_HIOS = 12,
STT_LOPROC = 13,
STT_SPARC_REGISTER = 13,
STT_HIPROC = 15,
// SHFs
SHF_ALLOC = 2,
SHF_EXECINSTR = 4,
// dynamic sections
DT_PLTGOT = 3,
DT_HASH = 4,
DT_STRTAB = 5,
DT_SYMTAB = 6,
DT_RELA = 7,
DT_RELASZ = 8,
DT_RELAENT = 9,
DT_INIT = 12,
DT_FINI = 13,
DT_REL = 17,
DT_RELSZ = 18,
DT_RELENT = 19,
DT_JMPREL = 23,
DT_INIT_ARRAY = 25,
DT_FINI_ARRAY = 26,
DT_INIT_ARRAYSZ = 27,
DT_PREINIT_ARRAY = 32,
DT_MOVETAB = 0x6ffffefe,
DT_VERDEF = 0x6ffffffc,
DT_VERNEED = 0x6ffffffe,
DT_SYMINFO = 0x6ffffeff,
// relocation types
R_ARM_ABS32 = 2,
R_ARM_REL32 = 3,
R_ARM_COPY = 20,
R_AARCH64_ABS64 = 0x101,
R_AARCH64_PREL64 = 0x104,
R_AARCH64_GLOB_DAT = 0x401,
R_AARCH64_JUMP_SLOT = 0x402,
R_AARCH64_RELATIVE = 0x403,
R_386_32 = 1,
R_386_PC32 = 2,
R_386_GLOB_DAT = 6,
R_386_JMP_SLOT = 7,
R_AMD64_64 = 1
}
#pragma warning disable CS0649
internal class elf_header<TWord> where TWord : struct
{
// 0x7f followed by ELF in ascii
public uint m_dwFormat;
// 1 - 32 bit
// 2 - 64 bit
public byte m_arch;
// 1 - little endian
// 2 - big endian
public byte m_endian;
// 1 is original elf format
public byte m_version;
// set based on OS, refer to OSABI enum
public byte m_osabi;
// refer to elf documentation
public byte m_osabi_ver;
// unused
[ArrayLength(FixedSize=7)]
public byte[] e_pad;//byte[7]
// 1 - relocatable
// 2 - executable
// 3 - shared
// 4 - core
public ushort e_type;
// refer to isa enum
public ushort e_machine;
public uint e_version;
public TWord e_entry;
public TWord e_phoff;
public TWord e_shoff;
public uint e_flags;
public ushort e_ehsize;
public ushort e_phentsize;
public ushort e_phnum;
public ushort e_shentsize;
public ushort e_shnum;
public ushort e_shtrndx;
}
internal interface Ielf_phdr<TWord> where TWord : struct
{
uint p_type { get; }
TWord p_offset { get; set; }
TWord p_filesz { get; set; }
TWord p_memsz { get; }
TWord p_vaddr { get; set; }
uint p_flags { get; }
}
internal class elf_32_phdr : Ielf_phdr<uint> {
public uint p_type => f_p_type;
public uint p_offset { get => f_p_offset; set => f_p_offset = value; }
public uint p_filesz { get => f_p_filesz; set => f_p_filesz = value; }
public uint p_vaddr { get => f_p_vaddr; set => f_p_vaddr = value; }
public uint p_flags => f_p_flags;
public uint p_memsz => f_p_memsz;
public uint f_p_type;
public uint f_p_offset;
public uint f_p_vaddr;
public uint p_paddr;
public uint f_p_filesz;
public uint f_p_memsz;
public uint f_p_flags;
public uint p_align;
}
internal class elf_64_phdr : Ielf_phdr<ulong>
{
public uint p_type => f_p_type;
public ulong p_offset { get => f_p_offset; set => f_p_offset = value; }
public ulong p_filesz { get => f_p_filesz; set => f_p_filesz = value; }
public ulong p_memsz => f_p_memsz;
public ulong p_vaddr { get => f_p_vaddr; set => f_p_vaddr = value; }
public uint p_flags => f_p_flags;
public uint f_p_type;
public uint f_p_flags;
public ulong f_p_offset;
public ulong f_p_vaddr;
public ulong p_paddr;
public ulong f_p_filesz;
public ulong f_p_memsz;
public ulong p_align;
}
internal class elf_shdr<TWord> where TWord : struct
{
public uint sh_name;
public uint sh_type;
public TWord sh_flags;
public TWord sh_addr;
public TWord sh_offset;
public TWord sh_size;
public uint sh_link;
public uint sh_info;
public TWord sh_addralign;
public TWord sh_entsize;
}
internal interface Ielf_sym<TWord> where TWord : struct
{
uint st_name { get; }
TWord st_value { get; }
ushort st_shndx { get; }
Elf st_info { get; }
Elf type { get; }
}
internal class elf_32_sym : Ielf_sym<uint>
{
public uint st_name => f_st_name;
public uint st_value => f_st_value;
public ushort st_shndx => f_st_shndx;
public Elf st_info => (Elf) f_st_info;
public Elf type => (Elf) (f_st_info & 0xf);
public uint f_st_name;
public uint f_st_value;
public uint st_size;
public byte f_st_info;
public byte st_other;
public ushort f_st_shndx;
}
internal class elf_64_sym : Ielf_sym<ulong>
{
public uint st_name => f_st_name;
public ulong st_value => f_st_value;
public ushort st_shndx => f_st_shndx;
public Elf st_info => (Elf) f_st_info;
public Elf type => (Elf) (f_st_info & 0xf);
public uint f_st_name;
public byte f_st_info;
public byte st_other;
public ushort f_st_shndx;
public ulong f_st_value;
public ulong st_size;
}
internal class elf_dynamic<TWord> where TWord : struct
{
public TWord d_tag;
public TWord d_un;
}
internal class elf_rel<TWord> where TWord : struct
{
public TWord r_offset;
public TWord r_info;
}
internal class elf_rela<TWord> where TWord : struct
{
public TWord r_offset;
public TWord r_info;
public TWord r_addend;
}
#pragma warning restore CS0649
}

View File

@@ -0,0 +1,175 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
#pragma warning disable CS0649
// Structures and enums: https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h
public enum MachO : uint
{
MH_MAGIC = 0xfeedface,
MH_CIGAM = 0xcefaedfe,
MH_MAGIC_64 = 0xfeedfacf,
MH_CIGAM_64 = 0xcffaedfe,
MH_MIN_FILETYPE = 0x1,
MH_MAX_FILETYPE = 0xB,
LC_SEGMENT = 0x1,
LC_SYMTAB = 0x2,
LC_DYSYMTAB = 0xb,
LC_SEGMENT_64 = 0x19,
LC_ENCRYPTION_INFO = 0x21,
LC_DYLD_INFO = 0x22,
LC_DYLD_INFO_ONLY = 0x80000022,
LC_FUNCTION_STARTS = 0x26,
LC_ENCRYPTION_INFO_64 = 0x2C,
CPU_TYPE_X86 = 7,
CPU_TYPE_X86_64 = 0x01000000 + CPU_TYPE_X86,
CPU_TYPE_ARM = 12,
CPU_TYPE_ARM64 = 0x01000000 + CPU_TYPE_ARM,
}
[Flags]
public enum MachO_NType : byte
{
// n_type masks
N_STAB = 0xe0,
N_PEXT = 0x10,
N_TYPE = 0x0e,
N_EXT = 0x01,
// n_stab bits
N_UNDF = 0x00,
N_ABS = 0x02,
N_SECT = 0x0e,
N_PBUD = 0x0c,
N_INDR = 0x0a,
// n_type bits when N_STAB has some bits set
N_GSYM = 0x20,
N_FNAME = 0x22,
N_FUN = 0x24,
N_STSYM = 0x26,
N_BNSYM = 0x2E,
N_ENSYM = 0x4E,
N_SO = 0x64,
N_OSO = 0x66,
}
internal class MachOHeader<TWord> where TWord : struct
{
public uint Magic;
public uint CPUType;
public uint CPUSubType;
public uint FileType;
public uint NumCommands;
public uint SizeOfCommands;
public TWord Flags;
}
internal class MachOLoadCommand
{
public uint Command;
public uint Size;
}
internal class MachOSegmentCommand<TWord> where TWord : struct
{
// MachOLoadCommand
[String(FixedSize = 16)]
public string Name;
public TWord VirtualAddress;
public TWord VirtualSize;
public TWord ImageOffset;
public TWord ImageSize;
public uint VMMaxProt;
public uint VMInitProt;
public uint NumSections;
public uint Flags;
}
internal class MachOSection<TWord> where TWord : struct
{
[String(FixedSize = 16)]
public string Name;
[String(FixedSize = 16)]
public string SegmentName;
public TWord Address;
public TWord Size;
public uint ImageOffset;
public uint Align;
public uint ImageRelocOffset;
public int NumRelocEntries;
public uint Flags;
public uint Reserved1;
public TWord Reserved2;
}
internal class MachOLinkEditDataCommand
{
// MachOLoadCommand
public uint Offset;
public uint Size;
}
internal class MachODyldInfoCommand
{
public uint RebaseOffset;
public uint RebaseSize;
public uint BindOffset;
public uint BindSize;
public uint WeakBindOffset;
public uint WeakBindSize;
public uint LazyBindOffset;
public uint LazyBindSize;
public uint ExportOffset;
public uint ExportSize;
}
internal class MachOSymtabCommand
{
public uint SymOffset;
public uint NumSyms;
public uint StrOffset;
public uint StrSize;
}
internal class MachOEncryptionInfo
{
// MachOLoadCommand
public uint CryptOffset;
public uint CryptSize;
public uint CryptID;
}
internal class MachO_nlist<TWord> where TWord : struct
{
public MachO_NType n_type => (MachO_NType) f_n_type;
public uint n_strx;
public byte f_n_type;
public byte n_sect;
public ushort n_desc;
public TWord n_value;
}
internal class MachO_relocation_info
{
public int r_address;
public uint r_data;
public uint r_symbolnum => r_data & 0x00ffffff;
public bool r_pcrel => ((r_data >> 24) & 1) == 1;
public uint r_length => (r_data >> 25) & 3;
public bool r_extern => ((r_data >> 27) & 1) == 1;
public uint r_type => r_data >> 28;
}
}

View File

@@ -0,0 +1,173 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
// Source: https://github.com/dotnet/llilc/blob/master/include/clr/ntimage.h
public enum PE : uint
{
IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b,
IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b,
IMAGE_SCN_CNT_CODE = 0x00000020,
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040,
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080,
IMAGE_SCN_MEM_EXECUTE = 0x20000000,
IMAGE_SCN_MEM_READ = 0x40000000,
IMAGE_SCN_MEM_WRITE = 0x80000000
}
#pragma warning disable CS0649
// _IMAGE_FILE_HEADER
internal class COFFHeader
{
public ushort Machine;
public ushort NumberOfSections;
public uint TimeDateStamp;
public uint PointerToSymbolTable;
public uint NumberOfSymbols;
public ushort SizeOfOptionalHeader;
public ushort Characteristics;
}
// _IMAGE_OPTIONAL_HEADER
internal interface IPEOptHeader
{
PE ExpectedMagic { get; }
ushort Magic { get; }
ulong ImageBase { get; set; }
uint BaseOfCode { get; }
RvaEntry[] DataDirectory { get; }
}
internal class PEOptHeader32 : IPEOptHeader
{
public PE ExpectedMagic => PE.IMAGE_NT_OPTIONAL_HDR32_MAGIC;
public ushort Magic => f_Magic;
public ulong ImageBase { get => f_ImageBase; set => f_ImageBase = (uint) value; }
public uint BaseOfCode => f_BaseOfCode;
public RvaEntry[] DataDirectory => f_DataDirectory;
public ushort f_Magic;
public byte MajorLinkerVersion;
public byte MinorLinkerVersion;
public uint SizeOfCode;
public uint SizeOfInitializedData;
public uint SizeOfUninitializedData;
public uint AddressOfEntryPoint;
public uint f_BaseOfCode;
public uint BaseOfData;
public uint f_ImageBase;
public uint SectionAlignment;
public uint FileAlignment;
public ushort MajorOSVersion;
public ushort MinorOSVersion;
public ushort MajorImageVersion;
public ushort MinorImageVersion;
public ushort MajorSubsystemVersion;
public ushort MinorSubsystemVersion;
public uint Win32VersionValue;
public uint SizeOfImage;
public uint SizeOfHeaders;
public uint Checksum;
public ushort Subsystem;
public ushort DLLCharacteristics;
public uint SizeOfStackReserve;
public uint SizeOfStackCommit;
public uint SizeOfHeapReserve;
public uint SizeOfHeapCommit;
public uint LoaderFlags;
public uint NumberOfRvaAndSizes;
[ArrayLength(FieldName = "NumberOfRvaAndSizes")]
public RvaEntry[] f_DataDirectory;
}
// _IMAGE_OPTIONAL_HEADER64
internal class PEOptHeader64 : IPEOptHeader
{
public PE ExpectedMagic => PE.IMAGE_NT_OPTIONAL_HDR64_MAGIC;
public ushort Magic => f_Magic;
public ulong ImageBase { get => f_ImageBase; set => f_ImageBase = value; }
public uint BaseOfCode => f_BaseOfCode;
public RvaEntry[] DataDirectory => f_DataDirectory;
public ushort f_Magic;
public byte MajorLinkerVersion;
public byte MinorLinkerVersion;
public uint SizeOfCode;
public uint SizeOfInitializedData;
public uint SizeOfUninitializedData;
public uint AddressOfEntryPoint;
public uint f_BaseOfCode;
public ulong f_ImageBase;
public uint SectionAlignment;
public uint FileAlignment;
public ushort MajorOSVersion;
public ushort MinorOSVersion;
public ushort MajorImageVersion;
public ushort MinorImageVersion;
public ushort MajorSubsystemVersion;
public ushort MinorSubsystemVersion;
public uint Win32VersionValue;
public uint SizeOfImage;
public uint SizeOfHeaders;
public uint Checksum;
public ushort Subsystem;
public ushort DLLCharacteristics;
public ulong SizeOfStackReserve;
public ulong SizeOfStackCommit;
public ulong SizeOfHeapReserve;
public ulong SizeOfHeapCommit;
public uint LoaderFlags;
public uint NumberOfRvaAndSizes;
[ArrayLength(FieldName = "NumberOfRvaAndSizes")]
public RvaEntry[] f_DataDirectory;
}
internal class RvaEntry
{
public uint VirtualAddress;
public uint Size;
}
// _IMAGE_SECTION_HEADER
internal class PESection
{
public PE Characteristics => (PE) f_Characteristics;
[String(FixedSize=8)]
public string Name;
public uint VirtualSize; // Size in memory
public uint VirtualAddress; // Base address in memory (RVA)
public uint SizeOfRawData; // Size in file
public uint PointerToRawData; // Base address in file
public uint PointerToRelocations;
public uint PointerToLinenumbers;
public ushort NumberOfRelocations;
public ushort NumberOfLinenumbers;
public uint f_Characteristics;
}
// _IMAGE_EXPORT_DIRECTORY
internal class PEExportDirectory
{
public uint Characteristics;
public uint TimeDateStamp;
public ushort MajorVersion;
public ushort MinorVersion;
public uint Name;
public uint Base;
public uint NumberOfFunctions;
public uint NumberOfNames;
public uint AddressOfFunctions;
public uint AddressOfNames;
public uint AddressOfNameOrdinals;
}
#pragma warning restore CS0649
}

View File

@@ -0,0 +1,117 @@
/*
Copyright 2020 Katy Coe - http://www.hearthcode.org - http://www.djkaty.com
All rights reserved.
*/
using System;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
internal enum SElfConsts : uint
{
Magic = 0x1D3D154F
}
[Flags]
internal enum SElfEntryFlags : ulong
{
Ordered = 0x1,
Encrypted = 0x2,
Signed = 0x4,
Deflated = 0x8,
WindowMask = 0x700,
Blocks = 0x800,
BlockSizeMask = 0xF000,
Digests = 0x10000,
Extents = 0x20000,
SegmentIndexMask = 0x_000F_FFF0_0000
}
// SCE-specific definitions for e_type
internal enum SElfETypes : ushort
{
ET_SCE_EXEC = 0xFE00,
ET_SCE_RELEXEC = 0xFE04,
ET_SCE_STUBLIB = 0xFE0C,
ET_SCE_DYNEXEC = 0xFE10, // SCE EXEC_ASLR (PS4 Executable with ASLR)
ET_SCE_DYNAMIC = 0xFE18,
ET_SCE_IOPRELEXEC = 0xFF80,
ET_SCE_IOPRELEXEC2 = 0xFF81,
ET_SCE_EERELEXEC = 0xFF90,
ET_SCE_EERELEXEC2 = 0xFF91,
ET_SCE_PSPRELEXEC = 0xFFA0,
ET_SCE_PPURELEXEC = 0xFFA4,
ET_SCE_ARMRELEXEC = 0xFFA5,
ET_SCE_PSPOVERLAY = 0xFFA8
}
// SCE-specific definitions for program header type
internal enum SElfPTypes : uint
{
PT_SCE_RELA = 0x60000000,
PT_SCE_DYNLIBDATA = 0x61000000,
PT_SCE_PROCPARAM = 0x61000001,
PT_SCE_MODULE_PARAM = 0x61000002,
PT_SCE_RELRO = 0x61000010,
PT_SCE_COMMENT = 0x6FFFFF00,
PT_SCE_VERSION = 0x6FFFFF01
}
// Extended info types
internal enum SElfExInfoTypes
{
PTYPE_FAKE = 0x1,
PTYPE_NPDRM_EXEC = 0x4,
PTYPE_NPDRM_DYNLIB = 0x5,
PTYPE_SYSTEM_EXEC = 0x8,
PTYPE_SYSTEM_DYNLIB = 0x9,
PTYPE_HOST_KERNEL = 0xC,
PTYPE_SECURE_MODULE = 0xE,
PTYPE_SECURE_KERNEL = 0xF
}
#pragma warning disable CS0649
internal class SElfHeader
{
public uint Magic;
public byte Version;
public byte Mode;
public byte Endian;
public byte Attributes;
public uint KeyType;
public ushort HeaderSize;
public ushort MetadataSize;
public ulong FileSize;
public ushort NumberOfEntries;
public ushort Flags;
public uint Padding;
}
internal class SElfEntry
{
public ulong Flags;
public ulong FileOffset;
public ulong EncryptedCompressedSize;
public ulong MemorySize;
public bool IsEncrypted => (Flags & (ulong) SElfEntryFlags.Encrypted) != 0;
public bool IsDeflated => (Flags & (ulong) SElfEntryFlags.Deflated) != 0;
public bool HasBlocks => (Flags & (ulong) SElfEntryFlags.Blocks) != 0;
public ushort SegmentIndex => (ushort) ((Flags & (ulong) SElfEntryFlags.SegmentIndexMask) >> 20);
}
internal class SElfSCEData
{
public ulong ProductAuthID;
public ulong ProductType;
public ulong AppVersion;
public ulong FirmwareVersion;
[ArrayLength(FixedSize = 0x20)]
public byte[] ContentID; // Only if NPDRM
[ArrayLength(FixedSize = 0x20)]
public byte[] SHA256Digest;
}
#pragma warning restore CS0649
}

View File

@@ -0,0 +1,33 @@
/*
Copyright 2017 Katy Coe - http://www.hearthcode.org - http://www.djkaty.com
All rights reserved.
*/
namespace Il2CppInspector
{
#pragma warning disable CS0649
// Structures and enums: https://cocoaintheshell.whine.fr/2009/07/universal-binary-mach-o-format/
public enum UB : uint
{
FAT_MAGIC = 0xcafebabe
}
// Big-endian
internal class FatHeader
{
public uint Magic;
public uint NumArch;
}
// Big-endian
internal class FatArch
{
public uint CPUType;
public uint CPUSubType;
public uint Offset;
public uint Size;
public uint Align;
}
}

View File

@@ -0,0 +1,21 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
namespace Il2CppInspector
{
// Modifiers for use when loading binary files
public class LoadOptions
{
// For dumped ELF files, the virtual address to which we should rebase - ignored for other file types
// Use 2^64-1 to prevent rebasing on a dumped file
public ulong ImageBase { get; set; }
// For Linux process memory map inputs, we need the full path so we can find the .bin files
// For packed PE files, we need the full path to reload the file via Win32 API
// Ignored for all other cases
public string BinaryFilePath { get; set; }
}
}

View File

@@ -0,0 +1,293 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
internal class MachOReader32 : MachOReader<uint, MachOReader32, Convert32>
{
public override int Bits => 32;
protected override bool checkMagicLE(MachO magic) => magic == MachO.MH_MAGIC;
protected override bool checkMagicBE(MachO magic) => magic == MachO.MH_CIGAM;
protected override MachO lc_Segment => MachO.LC_SEGMENT;
public override uint MapVATR(ulong uiAddr) {
var section = machoSections.First(x => uiAddr >= x.Address && uiAddr <= x.Address + x.Size && x.Name != "__bss" && x.Name != "__common");
return (uint) uiAddr - (section.Address - section.ImageOffset);
}
public override ulong MapFileOffsetToVA(uint offset) {
var section = machoSections.First(x => offset >= x.ImageOffset && offset < x.ImageOffset + x.Size);
return section.Address + offset - section.ImageOffset;
}
}
internal class MachOReader64 : MachOReader<ulong, MachOReader64, Convert64>
{
public override int Bits => 64;
protected override bool checkMagicLE(MachO magic) => magic == MachO.MH_MAGIC_64;
protected override bool checkMagicBE(MachO magic) => magic == MachO.MH_CIGAM_64;
protected override MachO lc_Segment => MachO.LC_SEGMENT_64;
public override uint MapVATR(ulong uiAddr) {
var section = machoSections.First(x => uiAddr >= x.Address && uiAddr <= x.Address + x.Size && x.Name != "__bss" && x.Name != "__common");
return (uint) (uiAddr - (section.Address - section.ImageOffset));
}
public override ulong MapFileOffsetToVA(uint offset) {
var section = machoSections.First(x => offset >= x.ImageOffset && offset < x.ImageOffset + x.Size);
return section.Address + offset - section.ImageOffset;
}
}
// We need this convoluted generic TReader declaration so that "static T FileFormatReader.Load(Stream)"
// is inherited to MachOReader32/64 with a correct definition of T
internal abstract class MachOReader<TWord, TReader, TConvert> : FileFormatStream<TReader>
where TWord : struct
where TReader : FileFormatStream<TReader>
where TConvert : IWordConverter<TWord>, new()
{
private readonly TConvert conv = new TConvert();
private MachOHeader<TWord> header;
protected readonly List<MachOSection<TWord>> machoSections = new List<MachOSection<TWord>>();
private List<Section> sections = new List<Section>();
private MachOSection<TWord> funcTab;
private MachOSymtabCommand symTab;
private List<Export> exports = new List<Export>();
public override string DefaultFilename => "app";
public override string Format => "Mach-O " + (Bits == 32 ? "32-bit" : "64-bit");
public override string Arch => (MachO)header.CPUType switch
{
MachO.CPU_TYPE_ARM => "ARM",
MachO.CPU_TYPE_ARM64 => "ARM64",
MachO.CPU_TYPE_X86 => "x86",
MachO.CPU_TYPE_X86_64 => "x64",
_ => "Unsupported"
};
protected abstract bool checkMagicLE(MachO magic);
protected abstract bool checkMagicBE(MachO magic);
protected abstract MachO lc_Segment { get; }
protected override bool Init() {
// Detect endianness - default is little-endianness
MachO magic = (MachO)ReadUInt32();
if (checkMagicBE(magic))
Endianness = Endianness.Big;
if (!checkMagicBE(magic) && !checkMagicLE(magic))
return false;
header = ReadObject<MachOHeader<TWord>>(0);
// Must be of a known file type
if ((MachO)header.FileType < MachO.MH_MIN_FILETYPE || (MachO)header.FileType > MachO.MH_MAX_FILETYPE)
return false;
// Process load commands
for (var c = 0; c < header.NumCommands; c++) {
var startPos = Position;
var loadCommand = ReadObject<MachOLoadCommand>();
switch ((MachO) loadCommand.Command) {
// Segments
case MachO cmd when cmd == lc_Segment:
var segment = ReadObject<MachOSegmentCommand<TWord>>();
// Sections in each segment
for (int s = 0; s < segment.NumSections; s++) {
var section = ReadObject<MachOSection<TWord>>();
machoSections.Add(section);
// Create universal section
sections.Add(new Section {
VirtualStart = conv.ULong(section.Address),
VirtualEnd = conv.ULong(section.Address) + (uint) Math.Max(conv.Int(section.Size) - 1, 0),
ImageStart = section.ImageOffset,
ImageEnd = section.ImageOffset + (uint) Math.Max(conv.Int(section.Size) - 1, 0),
IsData = segment.Name == "__TEXT" || segment.Name == "__DATA",
IsExec = segment.Name == "__TEXT",
IsBSS = segment.Name == "__DATA" && (section.Name == "__bss" || section.Name == "__common"),
Name = section.Name
});
// First code section
if (section.Name == "__text") {
GlobalOffset = (ulong) Convert.ChangeType(section.Address, typeof(ulong)) - section.ImageOffset;
}
// Initialization (pre-main) functions
if (section.Name == "__mod_init_func") {
funcTab = section;
}
}
break;
// Location of static symbol table
case MachO.LC_SYMTAB:
symTab = ReadObject<MachOSymtabCommand>();
break;
case MachO.LC_DYSYMTAB:
// TODO: Implement Mach-O dynamic symbol table
break;
// Compressed dyld information
case MachO.LC_DYLD_INFO:
case MachO.LC_DYLD_INFO_ONLY:
var dyld = ReadObject<MachODyldInfoCommand>();
loadExportTrie(dyld.ExportOffset);
break;
// Encryption check
// If cryptid == 1, this binary is encrypted with FairPlay DRM
case MachO.LC_ENCRYPTION_INFO:
case MachO.LC_ENCRYPTION_INFO_64:
var encryptionInfo = ReadObject<MachOEncryptionInfo>();
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;
}
// There might be other data after the load command so always use the specified total size to step forwards
Position = startPos + loadCommand.Size;
}
// Must find __mod_init_func
if (funcTab == null)
return false;
// Process relocations
foreach (var section in machoSections) {
var rels = ReadArray<MachO_relocation_info>(section.ImageRelocOffset, section.NumRelocEntries);
// TODO: Implement Mach-O relocations
if (rels.Any()) {
Console.WriteLine("Mach-O file contains relocations (feature not yet implemented)");
break;
}
}
return true;
}
// Handle export trie
private void loadExportTrie(uint trieOffset, uint nodeOffset = 0, string partialSymbol = "") {
Position = trieOffset + nodeOffset;
var size = ULEB128.Decode(this);
// Terminal information
if (size != 0) {
var flags = ReadByte();
var symbolKind = flags & 0x03;
var symbolType = (flags >> 2) & 0x03;
// 0 = regular, 1 = weak, 2 = re-export, 3 = stub
switch (symbolType) {
case 0:
var address = ULEB128.Decode(this);
exports.Add(new Export {Name = partialSymbol, VirtualAddress = GlobalOffset + address});
break;
case 1:
var weakAddress = ULEB128.Decode(this);
exports.Add(new Export {Name = partialSymbol, VirtualAddress = GlobalOffset + weakAddress});
break;
case 2:
var ordinal = ULEB128.Decode(this);
var name = ReadNullTerminatedString();
break;
case 3:
var stubOffset = ULEB128.Decode(this);
var resolverOffset = ULEB128.Decode(this);
break;
}
}
var branchCount = ReadByte();
for (int branch = 0; branch < branchCount; branch++) {
var prefix = ReadNullTerminatedString();
var childNodeOffset = (uint) ULEB128.Decode(this);
var currentPosition = Position;
loadExportTrie(trieOffset, childNodeOffset, partialSymbol + prefix);
Position = currentPosition;
}
}
public override uint[] GetFunctionTable() => 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() {
var symbols = new Dictionary<string, Symbol>();
// https://opensource.apple.com/source/cctools/cctools-795/include/mach-o/nlist.h
// n_sect: https://opensource.apple.com/source/cctools/cctools-795/include/mach-o/stab.h
var symbolList = ReadArray<MachO_nlist<TWord>>(symTab.SymOffset, (int) symTab.NumSyms);
// This is a really naive implementation that ignores the values of n_type and n_sect
// which may affect the interpretation of n_value
foreach (var symbol in symbolList) {
Position = symTab.StrOffset + symbol.n_strx;
var name = (symbol.n_strx != 0) ? ReadNullTerminatedString() : "";
var value = (ulong) Convert.ChangeType(symbol.n_value, typeof(ulong));
// Ignore symbols with no address or name
if (value == 0 || name.Length == 0)
continue;
// Mask out the N_EXT and N_PEXT bits because we don't care about it
var ntype = (MachO_NType) ((byte)symbol.n_type & ~(byte)MachO_NType.N_EXT & ~(byte)MachO_NType.N_PEXT);
// For non-debugging symbols (no bits of N_STAB set), just leave the N_TYPE
// Otherwise leave the whole n_type field (with N_EXT and N_PEXT removed)
var dbg = (symbol.n_type & MachO_NType.N_STAB) != 0;
if (dbg)
if (ntype == MachO_NType.N_BNSYM || ntype == MachO_NType.N_ENSYM
|| ntype == MachO_NType.N_SO || ntype == MachO_NType.N_OSO)
continue;
var type = ntype == MachO_NType.N_FUN? SymbolType.Function
: ntype == MachO_NType.N_STSYM || ntype == MachO_NType.N_GSYM || ntype == MachO_NType.N_SECT? SymbolType.Name
: SymbolType.Unknown;
if (type == SymbolType.Unknown) {
Console.WriteLine($"Unknown symbol type: {((int) ntype):x2} {value:x16} " + CxxDemangler.CxxDemangler.Demangle(name));
}
// Ignore duplicates
symbols.TryAdd(name, new Symbol { Name = name, VirtualAddress = value, Type = type });
}
return symbols;
}
public override IEnumerable<Export> GetExports() => exports;
public override IEnumerable<Section> GetSections() => sections;
}
}

View File

@@ -0,0 +1,286 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
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
{
// References:
// PE Header file: https://github.com/dotnet/llilc/blob/master/include/clr/ntimage.h
// PE format specification: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format?redirectedfrom=MSDN
internal class PEReader : FileFormatStream<PEReader>
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private extern static IntPtr LoadLibrary(string lpLibFileName);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private extern static bool FreeLibrary(IntPtr hLibModule);
private COFFHeader coff;
private IPEOptHeader pe;
private PESection[] sections;
private uint pFuncTable;
private bool mightBePacked;
// Section types we need to rename in obfuscated binaries
private Dictionary<PE, string> wantedSectionTypes = new Dictionary<PE, string> {
[PE.IMAGE_SCN_MEM_READ | PE.IMAGE_SCN_MEM_EXECUTE | PE.IMAGE_SCN_CNT_CODE] = ".text",
[PE.IMAGE_SCN_MEM_READ | PE.IMAGE_SCN_CNT_INITIALIZED_DATA] = ".rdata",
[PE.IMAGE_SCN_MEM_READ | PE.IMAGE_SCN_MEM_WRITE | PE.IMAGE_SCN_CNT_INITIALIZED_DATA] = ".data"
};
public override string DefaultFilename => "GameAssembly.dll";
public override string Format => pe is PEOptHeader32 ? "PE32" : "PE32+";
public override string Arch => coff.Machine switch {
0x8664 => "x64", // IMAGE_FILE_MACHINE_AMD64
0x1C0 => "ARM", // IMAGE_FILE_MACHINE_ARM
0xAA64 => "ARM64", // IMAGE_FILE_MACHINE_ARM64
0x1C4 => "ARM", // IMAGE_FILE_MACHINE_ARMINT (Thumb-2)
0x14C => "x86", // IMAGE_FILE_MACHINE_I386
0x1C2 => "ARM", // IMAGE_FILE_MACHINE_THUMB (Thumb)
_ => "Unsupported"
};
// IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20B
// IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10B
// Could also use coff.Characteristics (IMAGE_FILE_32BIT_MACHINE) or coff.Machine
public override int Bits => (PE) pe.Magic == PE.IMAGE_NT_OPTIONAL_HDR64_MAGIC ? 64 : 32;
public override ulong ImageBase => pe.ImageBase;
protected override bool Init() {
// Check for MZ signature "MZ"
if (ReadUInt16() != 0x5A4D)
return false;
// Get offset to PE header from DOS header
Position = ReadUInt32(0x3C);
// Check PE signature "PE\0\0"
if (ReadUInt32() != 0x00004550)
return false;
// Read COFF Header
coff = ReadObject<COFFHeader>();
// Ensure presence of PE Optional header
// Size will always be 0x60 (32-bit) or 0x70 (64-bit) + (0x10 ' 0x8) for 16 RVA entries @ 8 bytes each
if (!((coff.SizeOfOptionalHeader == 0xE0 ? 32 :
coff.SizeOfOptionalHeader == 0xF0 ? (int?) 64 : null) is var likelyWordSize))
return false;
// Read PE optional header
pe = likelyWordSize switch {
32 => ReadObject<PEOptHeader32>(),
64 => ReadObject<PEOptHeader64>(),
_ => null
};
// Confirm architecture magic number matches expected word size
if ((PE) pe.Magic != pe.ExpectedMagic)
return false;
// Get IAT
var IATStart = pe.DataDirectory[12].VirtualAddress;
var IATSize = pe.DataDirectory[12].Size;
// Get sections table
sections = ReadArray<PESection>(coff.NumberOfSections);
// 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");
// Packed with anything (including Themida)?
mightBePacked = sections.FirstOrDefault(x => x.Name == ".rdata") is null;
// Rename sections if needed (before potentially searching them or rewriting them to the stream)
foreach (var section in sections.Where(s => wantedSectionTypes.Keys.Contains(s.Characteristics)))
// Replace section name if blank or all whitespace
if (Regex.IsMatch(section.Name, @"^\s*$"))
section.Name = wantedSectionTypes[section.Characteristics];
// Get base of code
GlobalOffset = pe.ImageBase + pe.BaseOfCode - sections.First(x => x.Name == ".text").PointerToRawData;
// Confirm that .rdata section begins at same place as IAT
var rData = sections.First(x => x.Name == ".rdata");
mightBePacked |= rData.VirtualAddress != IATStart;
// 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)
pFuncTable += 4;
pFuncTable += 4;
}
else {
while (ReadUInt64() != 0)
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
return true;
}
// Load DLL into memory and save it as a new PE stream
private void load() {
// Check that the process is running in the same word size as the DLL
// One way round this in future would be to spawn a new process of the correct word size
if ((Environment.Is64BitProcess && Bits == 32) || (!Environment.Is64BitProcess && Bits == 64))
throw new InvalidOperationException($"Cannot unpack a {Bits}-bit DLL from within a {(Environment.Is64BitProcess ? 64 : 32)}-bit process. Use the {Bits}-version of Il2CppInspector to unpack this DLL.");
// Get file path
// This error should never occur with the bundled CLI and GUI; only when used as a library by a 3rd party tool
if (!(LoadOptions.BinaryFilePath is string dllPath))
throw new InvalidOperationException("To load a packed PE file, you must specify the DLL file path in LoadOptions");
// Attempt to load DLL and run startup functions
// NOTE: This can cause a CSE (AccessViolation) for certain types of protection
// so only try to unpack as the final load strategy
IntPtr hModule = LoadLibrary(dllPath);
if (hModule == IntPtr.Zero) {
var lastErrorCode = Marshal.GetLastWin32Error();
throw new InvalidOperationException($"Unable to load the DLL for unpacking: error code {lastErrorCode}");
}
// Maximum image size
var size = sections.Last().VirtualAddress + sections.Last().VirtualSize;
// Allocate memory for unpacked image
var peBytes = new byte[size];
// Copy relevant sections from unmanaged memory
foreach (var section in sections.Where(s => wantedSectionTypes.Keys.Contains(s.Characteristics)))
Marshal.Copy(IntPtr.Add(hModule, (int) section.VirtualAddress), peBytes, (int) section.VirtualAddress, (int) section.VirtualSize);
// Decrease reference count for unload
FreeLibrary(hModule);
// Rebase
pe.ImageBase = (ulong) hModule.ToInt64();
// Rewrite sections to match memory layout
foreach (var section in sections) {
section.PointerToRawData = section.VirtualAddress;
section.SizeOfRawData = section.VirtualSize;
}
// Truncate memory stream at start of COFF header
var endOfSignature = ReadUInt32(0x3C) + 4; // DOS header + 4-byte PE signature
SetLength(endOfSignature);
// Re-write the stream (the headers are only necessary in case the user wants to save)
Position = endOfSignature;
WriteObject(coff);
if (Bits == 32) WriteObject((PEOptHeader32) pe);
else WriteObject((PEOptHeader64) pe);
WriteArray(sections);
Write(peBytes, (int) Position, peBytes.Length - (int) Position);
IsModified = true;
}
// Raw file / unpacked file load strategies
public override IEnumerable<IFileFormatStream> TryNextLoadStrategy() {
// First load strategy: the regular file
yield return this;
// Second load strategy: load the DLL into memory to unpack it
if (mightBePacked) {
Console.WriteLine("IL2CPP binary appears to be packed - attempting to unpack and retrying");
StatusUpdate("Unpacking binary");
load();
yield return this;
}
}
public override uint[] GetFunctionTable() {
if (pFuncTable == 0)
return Array.Empty<uint>();
Position = pFuncTable;
var addrs = new List<uint>();
ulong addr;
// Use TryMapVATR to avoid crash if function table is stripped or corrupted
// Can happen with packed or previously packed files
while ((addr = pe is PEOptHeader32? ReadUInt32() : ReadUInt64()) != 0 && TryMapVATR(addr, out uint fileOffset))
addrs.Add(fileOffset & 0xfffffffc);
return addrs.ToArray();
}
public override IEnumerable<Export> GetExports() {
// Get exports table
var ETStart = pe.DataDirectory[0].VirtualAddress + pe.ImageBase;
// Get export RVAs
var exportDirectoryTable = ReadObject<PEExportDirectory>(MapVATR(ETStart));
var exportCount = (int) exportDirectoryTable.NumberOfFunctions;
var exportAddresses = ReadArray<uint>(MapVATR(exportDirectoryTable.AddressOfFunctions + pe.ImageBase), exportCount);
var exports = exportAddresses.Select((a, i) => new Export {
Ordinal = (int) (exportDirectoryTable.Base + i),
VirtualAddress = GlobalOffset + a
}).ToDictionary(x => x.Ordinal, x => x);
// Get export names
var nameCount = (int) exportDirectoryTable.NumberOfNames;
var namePointers = ReadArray<uint>(MapVATR(exportDirectoryTable.AddressOfNames + pe.ImageBase), nameCount);
var ordinals = ReadArray<ushort>(MapVATR(exportDirectoryTable.AddressOfNameOrdinals + pe.ImageBase), nameCount);
for (int i = 0; i < nameCount; i++) {
var name = ReadNullTerminatedString(MapVATR(namePointers[i] + pe.ImageBase));
var ordinal = (int) exportDirectoryTable.Base + ordinals[i];
exports[ordinal].Name = name;
}
return exports.Values;
}
public override uint MapVATR(ulong uiAddr) {
if (uiAddr == 0)
return 0;
var section = sections.First(x => uiAddr - pe.ImageBase >= x.VirtualAddress &&
uiAddr - pe.ImageBase < x.VirtualAddress + x.SizeOfRawData);
return (uint) (uiAddr - section.VirtualAddress - pe.ImageBase + section.PointerToRawData);
}
public override ulong MapFileOffsetToVA(uint offset) {
var section = sections.First(x => offset >= x.PointerToRawData && offset < x.PointerToRawData + x.SizeOfRawData);
return pe.ImageBase + section.VirtualAddress + offset - section.PointerToRawData;
}
public override IEnumerable<Section> GetSections() {
return sections.Select(s => new Section {
VirtualStart = pe.ImageBase + s.VirtualAddress,
VirtualEnd = pe.ImageBase + s.VirtualAddress + s.VirtualSize - 1,
ImageStart = s.PointerToRawData,
ImageEnd = s.PointerToRawData + s.SizeOfRawData - 1,
IsData = (s.Characteristics & PE.IMAGE_SCN_CNT_INITIALIZED_DATA) != 0,
IsExec = (s.Characteristics & PE.IMAGE_SCN_CNT_CODE) != 0,
IsBSS = (s.Characteristics & PE.IMAGE_SCN_CNT_UNINITIALIZED_DATA) != 0 || s.PointerToRawData == 0u,
Name = s.Name
});
}
}
}

View File

@@ -0,0 +1,113 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
// This is a wrapper for a Linux memory dump
// The supplied file is a text file containing the output of "cat /proc/["self"|process-id]/maps"
// We re-construct libil2cpp.so from the *.bin files and return it as the first image
internal class ProcessMapReader : FileFormatStream<ProcessMapReader>
{
private MemoryStream il2cpp;
public override string DefaultFilename => "maps.txt";
protected override bool Init() {
// Maps.txt is extremely unlikely to be larger than this, so don't waste time loading many megabytes of binary data for no reason
if (Length > 256 * 1024)
return false;
// Get the entire stream as a string
var text = System.Text.Encoding.ASCII.GetString(ToArray());
// Line format is: https://stackoverflow.com/questions/1401359/understanding-linux-proc-id-maps
// xxxxxxxx-yyyyyyyy ffff zzzzzzzz aa:bb c [whitespace] [image path]
// Where x = the start address
// Where y = the end address
// Where f = permission flags (rwxp or -)
// Where z = offset in file that the region was mapped from (NOTE: we ignore this and assume it's a contiguous run)
// Where aa:bb = device ID
// Where c = inode
var rgxProc = new Regex(@"^(?<start>[0-9A-Fa-f]{8})-(?<end>[0-9A-Fa-f]{8}) [rwxp\-]{4} [0-9A-Fa-f]{8} [0-9A-Fa-f]{2}:[0-9A-Fa-f]{2} \d+\s+(?<path>\S+)$", RegexOptions.Multiline);
// Determine where libil2cpp.so was mapped into memory
var il2cppMemory = rgxProc.Matches(text)
.Where(m => m.Groups["path"].Value.EndsWith("libil2cpp.so"))
.Select(m => new { Start = Convert.ToUInt32(m.Groups["start"].Value, 16),
End = Convert.ToUInt32(m.Groups["end"].Value, 16) }).ToList();
if (il2cppMemory.Count == 0)
return false;
// Get file path
// This error should never occur with the bundled CLI and GUI; only when used as a library by a 3rd party tool
if (!(LoadOptions.BinaryFilePath is string mapsPath))
throw new InvalidOperationException("To load a Linux process map, you must specify the maps file path in LoadOptions");
if (!mapsPath.ToLower().EndsWith("-maps.txt"))
throw new InvalidOperationException("To load a Linux process map, the map file must not be renamed");
var mapsDir = Path.GetDirectoryName(mapsPath);
var mapsPrefix = Path.GetFileName(mapsPath[..^9]);
// Get memory dump filenames and mappings
var rgxFile = new Regex(@"^\S+?-(?<start>[0-9A-Za-z]{8})-(?<end>[0-9A-Za-z]{8})\.bin$");
var files = Directory.GetFiles(mapsDir, mapsPrefix + "-*.bin")
.Select(f => rgxFile.Match(f))
.Where(m => m.Groups[0].Success)
.Select(m => new {
Start = Convert.ToUInt32(m.Groups["start"].Value, 16),
End = Convert.ToUInt32(m.Groups["end"].Value, 16),
Name = m.Groups[0].Value
}).OrderBy(m => m.Start).ToList();
// Determine which files contain libil2cpp.so
var neededFiles = files.Where(f => il2cppMemory.Any(m => f.Start < m.End && f.End > m.Start)).OrderBy(f => f.Start).ToList();
// Determine how much to trim from the start of the first file and the end of the last file
var offsetFirst = il2cppMemory.First().Start - neededFiles.First().Start;
var lengthLast = il2cppMemory.Last().End - neededFiles.Last().Start;
// Merge the files
il2cpp = new MemoryStream();
for (var i = 0; i < neededFiles.Count; i++) {
var offset = (i == 0)? offsetFirst : 0;
var length = ((i == neededFiles.Count - 1)? lengthLast : neededFiles[i].End - neededFiles[i].Start) - offset;
using var source = File.Open(neededFiles[i].Name, FileMode.Open, FileAccess.Read, FileShare.Read);
// Can't use Stream.CopyTo as it doesn't support length parameter
var buffer = new byte[length];
source.Position = offset;
source.Read(buffer, 0, (int) length);
il2cpp.Write(buffer);
}
// Set image base address for ELF loader
// ELF loader will rebase the image and mark it as modified for saving
LoadOptions.ImageBase = il2cppMemory.First().Start;
return true;
}
public override IFileFormatStream this[uint index] {
get {
// Get merged stream as ELF file
return (IFileFormatStream) ElfReader32.Load(il2cpp, LoadOptions, OnStatusUpdate) ?? ElfReader64.Load(il2cpp, LoadOptions, OnStatusUpdate);
}
}
}
}

View File

@@ -0,0 +1,130 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
// Sony PlayStation 4 fake signed ELF reader
// Not compatible with PlayStation 3, PSP or Vita
// References:
// http://hitmen.c02.at/files/yapspd/psp_doc/chap26.html
// https://www.psdevwiki.com/ps3/SELF_-_SPRX#File_Format
// https://www.psdevwiki.com/ps4/SELF_File_Format
// https://www.psxhax.com/threads/ps4-self-spkg-file-format-documentation-detailed-for-scene-devs.6636/
// https://wiki.henkaku.xyz/vita/images/a/a2/Vita_SDK_specifications.pdf
// https://www.psxhax.com/threads/make-fself-gui-for-flat_zs-make_fself-py-script-by-cfwprophet.3494/
internal class SElfReader : FileFormatStream<SElfReader>
{
public override string DefaultFilename => "libil2cpp.so";
public override string Format => sceData.ProductType == (ulong) SElfExInfoTypes.PTYPE_FAKE? "FSELF" : "SELF";
public override string Arch => "x64";
public override int Bits => 64;
private SElfHeader selfHeader;
private SElfEntry[] entries;
private SElfSCEData sceData;
private elf_header<ulong> elfHeader;
private elf_64_phdr[] pht;
private elf_64_phdr getProgramHeader(Elf programIndex) => pht.FirstOrDefault(x => x.p_type == (uint) programIndex);
protected override bool Init() {
selfHeader = ReadObject<SElfHeader>();
// Check for magic bytes
if ((SElfConsts) selfHeader.Magic != SElfConsts.Magic)
return false;
if (selfHeader.Endian != 0x1)
Endianness = Endianness.Big;
// Read entries
entries = ReadArray<SElfEntry>(selfHeader.NumberOfEntries);
// We can't deal with encrypted or compressed segments right now
if (entries.Any(e => e.HasBlocks && e.IsEncrypted))
throw new NotImplementedException("This file contains encrypted segments not currently supported by Il2CppInspector.");
if (entries.Any(e => e.HasBlocks && e.IsDeflated))
throw new NotImplementedException("This file contains compressed segments not currently supported by Il2CppInspector.");
// Read ELF header
// PS4 files as follows:
// m_arch = 0x2 (64-bit)
// m_endian = 0x1 (little endian)
// m_version = 0x1 (ELF version 1)
// m_osabi = 0x9 (FreeBSD)
// e_type = special type, see psdevwiki documentation; probably 0xFE10 or 0xFE18
// e_machine = 0x3E (x86-64)
var startOfElf = Position;
elfHeader = ReadObject<elf_header<ulong>>();
// Must be one of these supported binary types
if (elfHeader.e_type != (ushort) Elf.ET_EXEC
&& elfHeader.e_type != (ushort) SElfETypes.ET_SCE_EXEC
&& elfHeader.e_type != (ushort) SElfETypes.ET_SCE_DYNEXEC
&& elfHeader.e_type != (ushort) SElfETypes.ET_SCE_DYNAMIC)
return false;
// There are no sections, but read all the program headers
// Each segment of type PT_LOAD, PT_SCE_RELRO, PT_SCE_DYNLIBDATA and PT_SCE_COMMENT
// generates two SELF entries above - one pointing to the ELF segment and one pointing to a digest.
// Only p_vaddr is used for memory mapping; all other fields are ignored.
// offset, memsz and filesz are taken from the SELF entries.
// The digests are all-zero in FSELF files.
// All other ELF segments are ignored completely.
pht = ReadArray<elf_64_phdr>(startOfElf + (long) elfHeader.e_phoff, elfHeader.e_phnum);
// Read extended info
sceData = ReadObject<SElfSCEData>(startOfElf + (long) elfHeader.e_phoff + elfHeader.e_phentsize * elfHeader.e_phnum);
// Get SELF entries which point to segments defined in phdrs
var dataEntries = entries.Where(e => e.HasBlocks).ToList();
// Fixup the used phdr entries
foreach (var entry in dataEntries) {
pht[entry.SegmentIndex].f_p_filesz = entry.EncryptedCompressedSize;
pht[entry.SegmentIndex].f_p_offset = entry.FileOffset;
pht[entry.SegmentIndex].f_p_memsz = entry.MemorySize;
}
// Filter out unused phdr entries
var phdrIndices = dataEntries.Select(e => (int) e.SegmentIndex).ToList();
pht = pht.Where((e, i) => phdrIndices.Contains(i)).ToArray();
// Get offset of code section
var codeSegment = pht.First(x => ((Elf) x.p_flags & Elf.PF_X) == Elf.PF_X);
GlobalOffset = codeSegment.p_vaddr - codeSegment.p_offset;
return true;
}
// Only the DT_INIT function equivalent
public override uint[] GetFunctionTable() => new [] { MapVATR(elfHeader.e_entry) };
public override uint MapVATR(ulong uiAddr) {
var program_header_table = pht.First(x => uiAddr >= x.p_vaddr && uiAddr <= x.p_vaddr + x.p_filesz);
return (uint) (uiAddr - (program_header_table.p_vaddr - program_header_table.p_offset));
}
public override ulong MapFileOffsetToVA(uint offset) {
var segment = pht.First(x => offset >= x.p_offset && offset < x.p_offset + x.p_filesz);
return segment.p_vaddr + offset - segment.p_offset;
}
// TODO: SElfReader.GetSections()
}
}

View File

@@ -0,0 +1,21 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
namespace Il2CppInspector
{
// A code file function export
public class Section
{
public ulong VirtualStart;
public ulong VirtualEnd;
public uint ImageStart;
public uint ImageEnd;
public bool IsExec;
public bool IsData;
public bool IsBSS;
public string Name;
}
}

View File

@@ -0,0 +1,26 @@
/*
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
namespace Il2CppInspector
{
public enum SymbolType
{
Function,
Name,
Import,
Unknown
}
// A code file function export
public class Symbol
{
public ulong VirtualAddress { get; set; }
public string Name { get; set; }
public SymbolType Type { get; set; }
public string DemangledName => CxxDemangler.CxxDemangler.Demangle(Name);
}
}

View File

@@ -0,0 +1,46 @@
/*
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System.IO;
using NoisyCowStudios.Bin2Object;
namespace Il2CppInspector
{
internal class UBReader : FileFormatStream<UBReader>
{
private FatHeader header;
public override string DefaultFilename => "app";
protected override bool Init() {
// Fat headers are always big-endian regardless of architectures
Endianness = Endianness.Big;
header = ReadObject<FatHeader>();
if ((UB) header.Magic != UB.FAT_MAGIC)
return false;
NumImages = header.NumArch;
return true;
}
public override IFileFormatStream this[uint index] {
get {
Position = 0x8 + 0x14 * index; // sizeof(FatHeader), sizeof(FatArch)
Endianness = Endianness.Big;
var arch = ReadObject<FatArch>();
Position = arch.Offset;
Endianness = Endianness.Little;
using var s = new MemoryStream(ReadBytes((int) arch.Size));
return (IFileFormatStream) MachOReader32.Load(s, LoadOptions, OnStatusUpdate) ?? MachOReader64.Load(s, LoadOptions, OnStatusUpdate);
}
}
}
}

View File

@@ -0,0 +1,57 @@
/*
Copyright 2019-2020 Katy Coe - http://www.hearthcode.org - http://www.djkaty.com
All rights reserved.
*/
using System;
namespace Il2CppInspector
{
// NOTE: What we really should have done here is add a TWord type parameter to FileFormatReader<T>
// then we could probably avoid most of this
interface IWordConverter<TWord> where TWord : struct
{
TWord Add(TWord a, TWord b);
TWord Sub(TWord a, TWord b);
TWord Div(TWord a, TWord b);
TWord Div(TWord a, int b);
TWord FromUInt(uint a);
TWord FromULong(ulong a);
int Int(TWord a);
long Long(TWord a);
ulong ULong(TWord a);
bool Gt(TWord a, TWord b);
uint[] UIntArray(TWord[] a);
}
internal class Convert32 : IWordConverter<uint>
{
public uint Add(uint a, uint b) => a + b;
public uint Sub(uint a, uint b) => a - b;
public uint Div(uint a, uint b) => a / b;
public uint Div(uint a, int b) => a / (uint)b;
public uint FromUInt(uint a) => a;
public uint FromULong(ulong a) => (uint) a;
public int Int(uint a) => (int)a;
public long Long(uint a) => a;
public ulong ULong(uint a) => a;
public bool Gt(uint a, uint b) => a > b;
public uint[] UIntArray(uint[] a) => a;
}
internal class Convert64 : IWordConverter<ulong>
{
public ulong Add(ulong a, ulong b) => a + b;
public ulong Sub(ulong a, ulong b) => a - b;
public ulong Div(ulong a, ulong b) => a / b;
public ulong Div(ulong a, int b) => a / (uint)b;
public ulong FromUInt(uint a) => a;
public ulong FromULong(ulong a) => a;
public int Int(ulong a) => (int)a;
public long Long(ulong a) => (long)a;
public ulong ULong(ulong a) => a;
public bool Gt(ulong a, ulong b) => a > b;
public uint[] UIntArray(ulong[] a) => Array.ConvertAll(a, x => (uint)x);
}
}