diff --git a/Il2CppInspector.Common/FileFormatStreams/ElfReader.cs b/Il2CppInspector.Common/FileFormatStreams/ElfReader.cs index 3d11ad6..6d32b82 100644 --- a/Il2CppInspector.Common/FileFormatStreams/ElfReader.cs +++ b/Il2CppInspector.Common/FileFormatStreams/ElfReader.cs @@ -342,222 +342,9 @@ namespace Il2CppInspector // Build symbol and export tables processSymbols(); - // Detect and defeat various kinds of XOR encryption - StatusUpdate("Detecting encryption"); - - if (GetDynamicEntry(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 { - [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(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 = 2; - 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(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(); - 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."); - } - 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