ELF: Abstract XOR decryption to plugin

This commit is contained in:
Katy Coe
2021-01-12 01:19:31 +01:00
parent a9ab4e627c
commit 59c095f794

View File

@@ -342,222 +342,9 @@ namespace Il2CppInspector
// Build symbol and export tables // Build symbol and export tables
processSymbols(); 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<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 = 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<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.");
}
return true; 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 // Rebase the image to a new virtual address
private void rebase(TWord imageBase) { private void rebase(TWord imageBase) {
// Rebase PHT // Rebase PHT