ELF: ARMv8 XOR decryption support + heuristic improvements
This commit is contained in:
@@ -275,50 +275,88 @@ namespace Il2CppInspector
|
|||||||
}
|
}
|
||||||
Console.WriteLine($"Processed {rels.Count} relocations");
|
Console.WriteLine($"Processed {rels.Count} relocations");
|
||||||
|
|
||||||
// Detect and defeat XOR encryption
|
// Detect and defeat various kinds of XOR encryption
|
||||||
StatusUpdate("Detecting encryption");
|
StatusUpdate("Detecting encryption");
|
||||||
|
|
||||||
if (getDynamic(Elf.DT_INIT) != null && sectionByName.ContainsKey(".rodata")) {
|
if (getDynamic(Elf.DT_INIT) != null && sectionByName.ContainsKey(".rodata")) {
|
||||||
// Use the data section to determine IF the file is obfuscated
|
// Use the data section to determine some possible keys
|
||||||
var rodataFirstBytes = ReadBytes(conv.Long(sectionByName[".rodata"].sh_offset), 256);
|
// If the data section uses striped encryption, bucketing the whole section will not give the correct key
|
||||||
var xorKeyCandidate = rodataFirstBytes.GroupBy(b => b).OrderByDescending(f => f.Count()).First().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];
|
||||||
|
|
||||||
// We examine the bottom nibble of the 2nd byte and top nibble of 4th byte
|
// We examine the bottom nibble of the 2nd byte and top nibble of 4th byte
|
||||||
// of the first 64 words (256 bytes) of .text. These values are expected to be primarily 0x0 and 0xE (ARM only)
|
// of the first 64 words (256 bytes) of .text. These values are expected to be primarily 0x0 and 0xE (ARM only)
|
||||||
var textFirstDWords = ReadArray<uint>(conv.Long(sectionByName[".text"].sh_offset), 64);
|
var textFirstDWords = ReadArray<uint>(conv.Long(sectionByName[".text"].sh_offset), 256);
|
||||||
var bottom = textFirstDWords.Select(w => (w >> 8) & 0xF).GroupBy(n => n).OrderByDescending(f => f.Count()).First().Key;
|
var bottom = textFirstDWords.Select(w => (w >> armNibbleB) & 0xF).GroupBy(n => n).OrderByDescending(f => f.Count()).First().Key;
|
||||||
var top = textFirstDWords.Select(w => w >> 28).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 xorKey = (byte) (((top << 4) ^ 0xE0) | bottom);
|
var xorKeyCandidateFromCode = (byte) (((top ^ armValueT) << 4) | (bottom ^ armValueB));
|
||||||
|
|
||||||
if (xorKeyCandidate != 0x00) {
|
if (xorKeyCandidateStriped != 0x00) {
|
||||||
|
|
||||||
// Some files may use a striped encryption whereby alternate blocks are encrypted and un-encrypted
|
// Some files may use a striped encryption whereby alternate blocks are encrypted and un-encrypted
|
||||||
// The first part of each section is always encrypted. Scan for the first unencrypted block and find its size
|
// The first part of each section is always encrypted. Scan for the first unencrypted block and find its size
|
||||||
// Limit ourselves to 128KB. If no stripe has been found by then, the whole section is probably encrypted
|
// Limit ourselves to maxSearchLength. If no stripe has been found by then, the whole section is probably encrypted
|
||||||
|
|
||||||
// We refer to issue #96 where the code uses striped encryption in 4KB blocks
|
// We refer to issue #96 where the code uses striped encryption in 4KB blocks
|
||||||
// We perform heuristics for 128-byte blocks below
|
// We perform heuristics for block of size blockSize below
|
||||||
var start = conv.Int(sectionByName[".text"].sh_offset);
|
var start = conv.Int(sectionByName[".text"].sh_offset);
|
||||||
var length = conv.Int(sectionByName[".text"].sh_size);
|
var length = conv.Int(sectionByName[".text"].sh_size);
|
||||||
var blockSize = 0x80;
|
var blockSize = 0x100;
|
||||||
var maxSearchLength = 128 * 1024;
|
var maxSearchLength = 128 * 1024;
|
||||||
var firstUnencrypted = 0xffffffff;
|
var firstUnencrypted = 0xffffffff;
|
||||||
var stripeSize = 0xffffffff;
|
var stripeSize = 0xffffffff;
|
||||||
var threshold = (blockSize / 4) / 2;
|
|
||||||
|
// At least this many instructions must pass the threshold
|
||||||
|
var threshold = (blockSize / 4) / 5;
|
||||||
|
|
||||||
|
// A stripe of encryption or non-encryption is considered to have ended when this many blocks in the opposite state are found
|
||||||
|
var maxBlocksInARow = 4;
|
||||||
|
|
||||||
|
// Align start position to search block size
|
||||||
|
if (conv.Int(sectionByName[".text"].sh_addr) % blockSize != 0)
|
||||||
|
start += blockSize - conv.Int(sectionByName[".text"].sh_addr) % blockSize;
|
||||||
|
|
||||||
|
var probablyEncryptedCount = 0;
|
||||||
|
var probablyUnencryptedCount = 0;
|
||||||
|
|
||||||
for (var pos = start; pos < start + maxSearchLength && stripeSize == 0xffffffff; pos += blockSize) {
|
for (var pos = start; pos < start + maxSearchLength && stripeSize == 0xffffffff; pos += blockSize) {
|
||||||
var size = Math.Min(blockSize, start + length - pos);
|
var size = Math.Min(blockSize, start + length - pos);
|
||||||
var dwords = ReadArray<uint>(pos, size / 4);
|
var dwords = ReadArray<uint>(pos, size / 4);
|
||||||
var count0 = dwords.Count(w => ((w >> 8) & 0xF) == 0x0);
|
var countB = dwords.Count(w => ((w >> armNibbleB) & 0xF) == armValueB);
|
||||||
var countE = dwords.Count(w => (w >> 28) == 0xE);
|
var countT = dwords.Count(w => (w >> armNibbleT) == armValueT);
|
||||||
var encrypted = countE < threshold && count0 < threshold;
|
var probablyEncrypted = countT < threshold && countB < threshold;
|
||||||
|
|
||||||
if (!encrypted && firstUnencrypted == 0xffffffff)
|
// Increment one or the other; reset the other one to zero
|
||||||
firstUnencrypted = (uint) pos;
|
probablyEncryptedCount = probablyEncrypted? probablyEncryptedCount + 1 : 0;
|
||||||
|
probablyUnencryptedCount = probablyEncryptedCount == 0 ? probablyUnencryptedCount + 1 : 0;
|
||||||
|
|
||||||
if (encrypted && firstUnencrypted != 0xffffffff)
|
if (probablyUnencryptedCount >= maxBlocksInARow && firstUnencrypted == 0xffffffff)
|
||||||
stripeSize = (uint) pos - firstUnencrypted;
|
firstUnencrypted = (uint) (pos - (probablyUnencryptedCount - 1) * blockSize);
|
||||||
|
|
||||||
|
if (probablyEncryptedCount >= maxBlocksInARow && firstUnencrypted != 0xffffffff)
|
||||||
|
stripeSize = (uint) (pos - firstUnencrypted - (probablyEncryptedCount - 1) * blockSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = stripeSize != 0xffffffff ? xorKeyCandidateStriped : xorKeyCandidateFull;
|
||||||
|
|
||||||
StatusUpdate("Decrypting");
|
StatusUpdate("Decrypting");
|
||||||
Console.WriteLine($"Performing XOR decryption (key: 0x{xorKey:X2}, stripe size: 0x{stripeSize:X4})");
|
Console.WriteLine($"Performing XOR decryption (key: 0x{xorKey:X2}, stripe size: 0x{stripeSize:X4})");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user