// Detect and defeat various kinds of XOR encryption public void PostProcessImage <T>(FileFormatStream <T> stream, PluginPostProcessImageEventInfo data) where T : FileFormatStream <T> { if (stream is ElfReader32 stream32) { elf32 = stream32; } else if (stream is ElfReader64 stream64) { elf64 = stream64; } else { return; } PluginServices.For(this).StatusUpdate("Detecting encryption"); this.stream = stream; sections = stream.GetSections().GroupBy(s => s.Name).ToDictionary(s => s.Key, s => s.First()); if (HasDynamicEntry(Elf.DT_INIT) && sections.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 = stream.ReadBytes(sections[".rodata"].ImageStart, sections[".rodata"].ImageLength); 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)> {
protected Il2CppBinary(IFileFormatStream stream, EventHandler <string> statusCallback = null) { Image = stream; OnStatusUpdate = statusCallback; DiscoverAPIExports(); }
protected Il2CppBinary(IFileFormatStream stream, uint codeRegistration, uint metadataRegistration, EventHandler <string> statusCallback = null) { Image = stream; OnStatusUpdate = statusCallback; DiscoverAPIExports(); TryPrepareMetadata(codeRegistration, metadataRegistration); }
// The method for ARM64: // - We want to extract values for CodeRegistration and MetadataRegistration from Il2CppCodegenRegistration(void) // - One of the functions supplied will be either Il2CppCodeGenRegistration or an initializer for Il2CppCodeGenRegistration.cpp // - The initializer (if present) loads a pointer to Il2CppCodegenRegistration in X1, if the function isn't in the function table // - Il2CppCodegenRegistration loads CodeRegistration into X0, MetadataRegistration into X1 and Il2CppCodeGenOptions into X2 // - Loads can be done either with ADRP+ADD (loads the address of the wanted struct) or ADRP+LDR (loads a pointer to the address which must be de-referenced) // - Loads do not need to be pairs of sequential instructions // - We need to sweep the whole function from the ADRP to the next B to find an ADD or LDR with a corresponding register protected override (ulong, ulong) ConsiderCode(IFileFormatStream image, uint loc) { // Load function into memory // In practice, the longest function length we need is not generally longer than 7 instructions (0x1C bytes) var func = getFunctionAtFileOffset(image, loc, 7); // Don't accept functions longer than 7 instructions (in this case, the last instruction won't be a B) if (!isB(func[^ 1]))
// Load binary with a global-metadata.dat available // Supplying the Metadata class when loading a binary is optional // If it is specified and both symbol table and function scanning fail, // Metadata will be used to try to find the required structures with data analysis // If it is not specified, data analysis will not be performed public static Il2CppBinary Load(IFileFormatStream stream, Metadata metadata, EventHandler <string> statusCallback = null) { foreach (var loadedImage in stream.TryNextLoadStrategy()) { var inst = LoadImpl(stream, statusCallback); if (inst.FindRegistrationStructs(metadata)) { return(inst); } } return(null); }
public static ulong Decode(IFileFormatStream next) { ulong uleb = 0; byte b = 0x80; for (var shift = 0; b >> 7 == 1; shift += 7) { b = next.ReadByte(); uleb |= (ulong)(b & 0x7f) << shift; } return(uleb); }
private List <uint> getFunctionAtFileOffset(IFileFormatStream image, uint loc, uint maxLength) { // Read a function that ends in a hard branch (B) or exceeds maxLength instructions var func = new List <uint>(); uint inst; image.Position = loc; do { inst = image.ReadUInt32(); func.Add(inst); } while (!isB(inst) && func.Count < maxLength); return(func); }
// Load and initialize a binary of any supported architecture private static Il2CppBinary LoadImpl(IFileFormatStream stream, EventHandler <string> statusCallback) { // Get type from image architecture var type = Assembly.GetExecutingAssembly().GetType("Il2CppInspector.Il2CppBinary" + stream.Arch.ToUpper()); if (type == null) { throw new NotImplementedException("Unsupported architecture: " + stream.Arch); } // Set width of long (convert to sizeof(int) for 32-bit files) if (stream[0].Bits == 32) { stream[0].AddPrimitiveMapping(typeof(long), typeof(int)); stream[0].AddPrimitiveMapping(typeof(ulong), typeof(uint)); } return((Il2CppBinary)Activator.CreateInstance(type, stream[0], statusCallback)); }
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); } }
public Il2CppBinaryARM(IFileFormatStream stream, uint codeRegistration, uint metadataRegistration, EventHandler <string> statusCallback = null) : base(stream, codeRegistration, metadataRegistration, statusCallback) { }
public Il2CppBinaryARM(IFileFormatStream stream, EventHandler <string> statusCallback = null) : base(stream, statusCallback) { }
// Architecture-specific search function protected abstract (ulong, ulong) ConsiderCode(IFileFormatStream image, uint loc);
// Attempt to guess the compiler used to build the binary via its file type public static CppCompilerType GuessFromImage(IFileFormatStream image) => (image is PEReader? CppCompilerType.MSVC : CppCompilerType.GCC);
protected override (ulong, ulong) ConsiderCode(IFileFormatStream image, uint loc) { // Setup var buffSize = 0x76; // minimum number of bytes to process the longest expected function var leaSize = 7; // the length of an LEA instruction with a 64-bit register operand and a 32-bit memory operand var xor64Size = 3; // the length of a XOR instruction of two 64-bit registers var xor32Size = 2; // the length of a XOR instruction of two 32-bit registers var pushSize = 2; // the length of a PUSH instruction with a 64-bit register var offset = 0; int RAX = 0, RBX = 3, RCX = 1, RDX = 2, RSI = 6, RDI = 7; // R8 = 8 ulong pCgr = 0; // the point to the code registration function image.Position = loc; var buff = image.ReadBytes(buffSize); // We have seen two versions of the initializer: // 1. Regular version // 2. Inlined version with il2cpp::utils::RegisterRuntimeInitializeAndCleanup(CallbackFunction, CallbackFunction, order) // Version 1 passes "this" in rcx and the arguments in rdx (our wanted pointer), r8d (always zero) and r9d (always zero) // or "this" in rdi, and the arguments in rsi (our wanted pointer), edx (always zero) and ecx (always zero) // Version 2 has a standard prologue and loads the wanted pointer into rax or rbp (lea rax/rbp) (int reg, uint operand)? lea; // Check for regular version // Generalize it as follows: // - each instruction must be lea r64, imm32 or xor r32, r32 // - xors must always have the same register for both operands // - lea that can't be mapped into the file is the pointer to 'this', otherwise it's the pointer to the init function // - the last instruction should always be jmp (not currently enforced) // - function length should not be longer than 5 instructions (two leas, two xors and one jmp) offset = 0; for (var instructions = 0; instructions < 4; instructions++) { // All allowed instruction types var xor32 = getXorR32R32(buff, offset); var xor64 = getXorR64R64(buff, offset); lea = getLea(buff, offset); if (xor32 != null && xor32.Value.reg_op1 == xor32.Value.reg_op2) { offset += xor32Size; } else if (xor64 != null && xor64.Value.reg_op1 == xor64.Value.reg_op2) { offset += xor64Size; } else if (lea != null) { offset += leaSize; if (pCgr == 0) { try { // We may have found Il2CppCodegenRegistration(void) pCgr = image.GlobalOffset + loc + (ulong)offset + lea.Value.operand; var newLoc = image.MapVATR(pCgr); } catch (InvalidOperationException) { // this pointer pCgr = 0; } } } else { // not lea or xor pCgr = 0; break; } } // Check for inlined version if (pCgr == 0) { // Check for prologue // - A sequence of 0 or more mov [rsp+argX], rXX followed by 1 or more push rXX offset = 0; while (isMovRM64R64(buff, offset)) { offset += 5; } if (isPushR64(buff, offset)) { // Linear sweep for LEA var leaInlined = findLea(buff, pushSize, buffSize - pushSize); if (leaInlined != null) { pCgr = image.GlobalOffset + loc + (uint)leaInlined.Value.foundOffset + (uint)leaSize + leaInlined.Value.operand; } } } // Assume we've found the pointer to Il2CppCodegenRegistration(void) and jump there if (pCgr != 0) { try { Image.Position = Image.MapVATR(pCgr); } // Couldn't map virtual address to data in file, so it's not this function catch (InvalidOperationException) { pCgr = 0; } } // Find the first 2 LEAs which we'll hope contain pointers to CodeRegistration and MetadataRegistration // There are two options here: // 1. il2cpp::vm::MetadataCache::Register is called directly with arguments in rcx, rdx, r8 or rdi, rsi, rdx (lea, lea, lea, jmp) // 2. The two functions being inlined. The arguments are loaded sequentially into rax after the prologue if (pCgr != 0) { var buff2Size = 0x50; var buff2 = image.ReadBytes(buffSize); offset = 0; var leas = new Dictionary <(int index, ulong address), int>(); // Find the first three LEAs in the function while (offset + leaSize < buff2Size && leas.Count < 3) { var nextLea = findLea(buff2, offset, buff2Size - (offset + leaSize)); // Use the original pointer found, not the file location + GlobalOffset because the data may be in a different section if (nextLea != null) { leas.Add((leas.Count, pCgr + (uint)nextLea.Value.foundOffset + (uint)leaSize + nextLea.Value.operand), nextLea.Value.reg); } offset = nextLea?.foundOffset + leaSize ?? buff2Size; } if ((image.Version < 21 && leas.Count == 2) || (image.Version >= 21 && leas.Count == 3)) { // Register-based argument passing? var leaRSI = leas.FirstOrDefault(l => l.Value == RSI).Key.address; var leaRDI = leas.FirstOrDefault(l => l.Value == RDI).Key.address; if (leaRSI != 0 && leaRDI != 0) { return(leaRDI, leaRSI); } var leaRCX = leas.FirstOrDefault(l => l.Value == RCX).Key.address; var leaRDX = leas.FirstOrDefault(l => l.Value == RDX).Key.address; if (leaRCX != 0 && leaRDX != 0) { return(leaRCX, leaRDX); } // RAX sequential loading? If so, take the first two arguments var leasRAX = leas.Where(l => l.Value == RAX).OrderBy(l => l.Key.index).Select(l => l.Key.address).ToArray(); if (leasRAX.Length > 1) { return(leasRAX[0], leasRAX[1]); } } } // If no initializer is found, we may be looking at a DT_INIT function which calls its own function table manually // In the sample we have seen (PlayStation 4), this function runs through two function tables: // 1. Start address of table loaded into rbx, pointer past end of table in r12 (lea rbx; lea r12) // 2. Pointer to final address of 2nd table loaded into rbx (lea rbx), runs backwards (8 bytes per entry) until finding 0xFFFFFFFF_FFFFFFFF // The strategy: find these LEAs, acquire and merge the two function tables, then call ourselves in a loop to check each function address // Expect function prologue and at least 3 64-bit register pushes (there are probably more) if (!isPrologue(buff) || !isPushR64(buff, 4) || !isPushR64(buff, 6) || !isPushR64(buff, 8)) { return(0, 0); } // Find the start and end addresses of the first function table var leaOfStart = findLea(buff, 10, buffSize - 10); if (leaOfStart == null || leaOfStart.Value.reg != RBX) // Most be lea rbx { return(0, 0); } var leaOfEnd = findLea(buff, leaOfStart.Value.foundOffset + leaSize, buffSize - (leaOfStart.Value.foundOffset + leaSize)); if (leaOfEnd == null || leaOfEnd.Value.reg == RBX) // Must be lea with any register besides rbx { return(0, 0); } var ptrStart1 = leaOfStart.Value.foundOffset + leaSize + leaOfStart.Value.operand; var ptrEnd1 = leaOfEnd.Value.foundOffset + leaSize + leaOfEnd.Value.operand; // Find the address of the last item in the second function table var leaOfLastItem = findLea(buff, leaOfEnd.Value.foundOffset + leaSize, buffSize - (leaOfEnd.Value.foundOffset + leaSize)); if (leaOfLastItem == null || leaOfLastItem.Value.reg != 0b11) // Must be lea rbx { return(0, 0); } var entrySize = 8; // 64-bit array entries var ptrEnd2 = leaOfLastItem.Value.foundOffset + leaSize + leaOfLastItem.Value.operand + entrySize; // Work backwards to find the address of the first item in the second function table var ptrStart2 = ptrEnd2; while (image.ReadUInt64(image.MapVATR((ulong)ptrStart2)) != 0xFFFF_FFFF_FFFF_FFFF) { ptrStart2 -= entrySize; } ptrStart2 += entrySize; // Acquire both function tables var funcs1 = image.ReadMappedWordArray((ulong)ptrStart1, (int)(ptrEnd1 - ptrStart1) / entrySize); var funcs2 = image.ReadMappedWordArray((ulong)ptrStart2, (int)(ptrEnd2 - ptrStart2) / entrySize); // Check every function var funcs = funcs1.Concat(funcs2); foreach (var pFunc in funcs) { var result = ConsiderCode(image, image.MapVATR((ulong)pFunc)); if (result != (0, 0)) { return(result); } } return(0, 0); }
protected override (ulong, ulong) ConsiderCode(IFileFormatStream image, uint loc) { ulong metadata, code; long pCgr; // x86 // Assembly bytes to search for at start of each function var bytes = new byte[] { 0x6A, 0x00, 0x6A, 0x00, 0x68 }; image.Position = loc; var buff = image.ReadBytes(5); if (bytes.SequenceEqual(buff)) { // Next 4 bytes are the function pointer being pushed onto the stack pCgr = image.ReadUInt32(); // Start of next instruction if (image.ReadByte() != 0xB9) { return(0, 0); } // Jump to Il2CppCodegenRegistration if (image.Version < 21) { image.Position = image.MapVATR((ulong)pCgr + 1); metadata = image.ReadUInt32(); image.Position = image.MapVATR((ulong)pCgr + 6); code = image.ReadUInt32(); } else { image.Position = image.MapVATR((ulong)pCgr + 6); metadata = image.ReadUInt32(); image.Position = image.MapVATR((ulong)pCgr + 11); code = image.ReadUInt32(); } return(code, metadata); } // x86 based on ELF PLT if (image is IElfReader elf) { var plt = elf.GetPLTAddress(); // push ebp; mov ebp, esp; push ebx; and esp, 0FFFFFFF0h; sub esp, 20h; call $+5; pop ebx bytes = new byte[] { 0x55, 0x89, 0xE5, 0x53, 0x83, 0xE4, 0xF0, 0x83, 0xEC, 0x20, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B }; image.Position = loc; buff = image.ReadBytes(16); if (!bytes.SequenceEqual(buff)) { return(0, 0); } // lea eax, (pCgr - offset)[ebx] (Position + 6 is the opcode lea eax; Position + 8 is the operand) image.Position += 6; // Ensure it's lea eax, #address if (image.ReadUInt16() != 0x838D) { return(0, 0); } try { pCgr = image.MapVATR(image.ReadUInt32() + plt); } // Could not find a mapping in the section table catch (InvalidOperationException) { return(0, 0); } // Extract Metadata pointer // An 0x838D opcode indicates LEA (no indirection) image.Position = pCgr + 0x20; var opcode = image.ReadUInt16(); metadata = image.ReadUInt32() + plt; // An 8x838B opcode indicates MOV (pointer indirection) if (opcode == 0x838B) { image.Position = image.MapVATR(metadata); metadata = image.ReadUInt32(); } if (opcode != 0x838B && opcode != 0x838D) { return(0, 0); } // Repeat the same logic for extracting the Code pointer image.Position = pCgr + 0x2A; opcode = image.ReadUInt16(); code = image.ReadUInt32() + plt; if (opcode == 0x838B) { image.Position = image.MapVATR(code); code = image.ReadUInt32(); } if (opcode != 0x838B && opcode != 0x838D) { return(0, 0); } return(code, metadata); } return(0, 0); }
private Dictionary <uint, ulong> sweepForAddressLoads(List <uint> func, ulong baseAddress, IFileFormatStream image) { // List of registers and addresses loaded into them var regs = new Dictionary <uint, ulong>(); // Iterate each instruction var pc = baseAddress; foreach (var inst in func) { // Is it an ADRP Xn, #page? if (getAdrp(inst, pc) is (uint reg, ulong page)) { // If we've had an earlier ADRP for the same register, we'll discard the previous load if (regs.ContainsKey(reg)) { regs[reg] = page; } else { regs.Add(reg, page); } } if (getAdr(inst, pc) is (uint reg_adr, ulong addr)) { if (regs.ContainsKey(reg_adr)) { regs[reg_adr] = addr; } else { regs.Add(reg_adr, addr); } } // Is it an ADD Xm, Xn, #offset? if (getAdd64(inst) is (uint reg_n, uint reg_d, uint imm)) { // We are only interested in registers that have already had an ADRP, and the ADD must be to itself if (reg_n == reg_d && regs.ContainsKey(reg_d)) { regs[reg_d] += imm; } } // Is it an LDR Xm, [Xn, #offset]? if (getLdr64ImmOffset(inst) is (uint reg_t, uint reg_ldr_n, uint simm)) { // We are only interested in registers that have already had an ADRP, and the LDR must be to itself if (reg_t == reg_ldr_n && regs.ContainsKey(reg_ldr_n)) { regs[reg_ldr_n] += simm * 8; // simm is a byte offset in a multiple of 8 // Now we have a pointer address, dereference it regs[reg_ldr_n] = image.ReadUInt64(image.MapVATR(regs[reg_ldr_n])); } } // Advance program counter which we need to calculate ADRP pages correctly pc += 4; } return(regs); }