diff --git a/README.md b/README.md index 48a0e4b..7beb809 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Options: --save-anyway Saves output of devirtualizer even if it fails [default: False] --only-save-devirted Only saves successfully devirtualized methods (This option only matters if you use the save anyway option) [default: False] --require-deps-for-generics Require dependencies when resolving generic methods for accuracy [default: True] + --hm-pass Homomorphic password(s) keyed by mdtoken, supporting multiple passwords per method with optional 1-based ordering. Formats: mdtoken:order:type:value | mdtoken:type:value. Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. String values must be wrapped in double quotes (\"...\") and may contain colons; escape double quotes and backslashes with a backslash. Strings use UTF-16. Repeatable; passwords are consumed in the specified order per method. --version Show version information -?, -h, --help Show help and usage information ``` @@ -43,6 +44,46 @@ Options: $ EazyDevirt.exe test.exe -v 3 --preserve-all --save-anyway true ``` +### Homomorphic Encryption passwords +You can either provide the passwords using the CLI or the interactive prompt. +If you're using the interactive prompt, you don't need to quote string values, but the rules below regarding the types and numeric values still apply. + +- Provide one or more passwords per method using the metadata token (hex, with or without `0x`). +- Typed-only: you must specify the type. Supported: `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `string`. +- Repeat the option for multiple passwords. Use optional 1-based `order` to control sequence when a method has multiple Homomorphic Encryption blocks. + +Formats: +- `mdtoken:order:type:value` +- `mdtoken:type:value` + +Validation rules: +- `mdtoken` must be hex (with or without `0x`), e.g. `0x06000123` or `06000123`. +- `order` (if present) must be a positive integer (1-based). +- `type` must be one of the supported types. Aliases are accepted: + - `sbyte`/`i8`, `byte`/`u8`, `short`/`int16`/`i16`, `ushort`/`uint16`/`u16`, + `int`/`int32`/`i32`, `uint`/`uint32`/`u32`, `long`/`int64`/`i64`, `ulong`/`uint64`/`u64`, `string`/`str`. +- `value` must match the specified type: + - Numerics: decimal (e.g. `1337`) or hex with `0x` prefix (e.g. `0xDEADBEEF`). + - String: any text wrapped in double quotes ("..."). Quotes are required; colons are allowed inside. Only double quotes and backslashes must be escaped via backslash (i.e., use `\"` for `"` and `\\` for `\`). Strings are encoded as UTF-16. +- If `order` is omitted, passwords are consumed in the order they appear on the CLI for that `mdtoken`. + +Examples: +```console +# Two explicitly ordered numeric passwords for method 0x06000123 +EazyDevirt.exe app.exe --hm-pass 0x06000123:1:uint:1234 --hm-pass 0x06000123:2:ulong:0xDEADBEEF + +# Multiple methods; string must be quoted and use UTF-16 +EazyDevirt.exe app.exe \ + --hm-pass 06000123:int:1337 \ + --hm-pass 06000456:string:"My Secret Password" + +# Strings with quotes inside (escape double quotes): +EazyDevirt.exe app.exe --hm-pass 06000456:string:"Password is: \"Hello\"" +``` + +It should be noted there are two additional password types that are not supported by [EazyDevirt]: `IEnumerable` and `byte[]`. +Feel free to make a PR if you need support for them. + ### Notes Don't rename any members before devirtualization, as [Eazfuscator.NET] resolves members using names rather than tokens. @@ -98,4 +139,4 @@ And a thank you, to [all other contributors](https://github.com/puff/EazyDevirt/ [AsmResolver]:https://github.com/Washi1337/AsmResolver [Echo]:https://github.com/Washi1337/Echo [Eazfuscator.NET]:https://www.gapotchenko.com/eazfuscator.net -[EazFixer]:https://github.com/holly-hacker/EazFixer +[EazFixer]:https://github.com/holly-hacker/EazFixer \ No newline at end of file diff --git a/src/EazyDevirt/Core/Architecture/VMMethod.cs b/src/EazyDevirt/Core/Architecture/VMMethod.cs index 24a46ca..5b45aeb 100644 --- a/src/EazyDevirt/Core/Architecture/VMMethod.cs +++ b/src/EazyDevirt/Core/Architecture/VMMethod.cs @@ -1,4 +1,4 @@ -using AsmResolver.DotNet; +using AsmResolver.DotNet; using AsmResolver.DotNet.Code.Cil; using AsmResolver.PE.DotNet.Cil; @@ -17,12 +17,19 @@ internal record VMMethod(MethodDefinition Parent, string EncodedMethodKey, long public List Locals { get; set; } public List Instructions { get; set; } - public bool SuccessfullyDevirtualized { get; set; } public bool HasHomomorphicEncryption { get; set; } public int CodeSize { get; set; } - public long CodePosition { get; set; } + public uint CurrentVirtualOffset { get; set; } + /// + /// Mapping from VM virtual offset to CIL offset, built once during instruction reading. + /// + public Dictionary VmToCilOffsetMap { get; set; } public long InitialCodeStreamPosition { get; set; } + /// + /// Stack holding Homomorphic Encryption ending positions. Used to calculate virtual <-> CIL offsets. + /// + public Stack HMEndPositionStack { get; set; } public override string ToString() => $"Parent: {Parent.MetadataToken} | EncodedMethodKey: {EncodedMethodKey} | MethodKey: 0x{MethodKey:X} | " + diff --git a/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs b/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs new file mode 100644 index 0000000..bd9ff10 --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs @@ -0,0 +1,108 @@ +using System.Security.Cryptography; + +namespace EazyDevirt.Core.Crypto; + +internal abstract class HMDecryptionChain +{ + private readonly SymmetricAlgorithm[] _algorithmChains; + + protected HMDecryptionChain(byte[] password, long salt) + : this(password, ConvertLongToLittleEndian(salt)) + { + } + + protected HMDecryptionChain(byte[] password, byte[] salt) + { + var pbkdf = new PBKDF2(password, salt, 1); + var array = new SymmetricAlgorithm[5]; + for (var i = 0; i < 5; i++) + { + var chain = new SymmetricAlgorithmChain(new Skip32Cipher()); + chain.Key = pbkdf.GetBytes(chain.KeySize / 8); + chain.IV = pbkdf.GetBytes(chain.GetIVSize() / 8); + array[i] = chain; + } + + _algorithmChains = array; + } + + protected static int AlignToMultipleOf4(int value) => (value + 3) / 4 * 4; + + public static int MinAlignToMultipleOf4(int value) => AlignToMultipleOf4(value + 4); + + protected static byte[] ConvertLongToLittleEndian(long value) + { + var output = new byte[8]; + ConvertLongToLittleEndian(value, output, 0); + return output; + } + + protected static void ConvertLongToLittleEndian(long value, byte[] output, int startIndex) + { + output[startIndex] = (byte)value; + output[startIndex + 1] = (byte)(value >> 8); + output[startIndex + 2] = (byte)(value >> 16); + output[startIndex + 3] = (byte)(value >> 24); + output[startIndex + 4] = (byte)(value >> 32); + output[startIndex + 5] = (byte)(value >> 40); + output[startIndex + 6] = (byte)(value >> 48); + output[startIndex + 7] = (byte)(value >> 56); + } + + protected static int ConvertInt32BytesToLittleEndian(byte[] bytes, int startIndex) + { + return bytes[startIndex] + | (bytes[startIndex + 1] << 8) + | (bytes[startIndex + 2] << 16) + | (bytes[startIndex + 3] << 24); + } + + protected static void ConvertInt32ToLittleEndian(int value, byte[] output, int startIndex) + { + output[startIndex] = (byte)value; + output[startIndex + 1] = (byte)(value >> 8); + output[startIndex + 2] = (byte)(value >> 16); + output[startIndex + 3] = (byte)(value >> 24); + } + + protected byte[] DecryptBytes(byte[] input, bool startWithEncrypt) + { + if (startWithEncrypt) + { + foreach (var alg in _algorithmChains) + { + if (startWithEncrypt) + { + using var enc = alg.CreateEncryptor(); + input = enc.TransformFinalBlock(input, 0, input.Length); + } + else + { + using var dec = alg.CreateDecryptor(); + input = dec.TransformFinalBlock(input, 0, input.Length); + } + startWithEncrypt = !startWithEncrypt; + } + } + else + { + for (int i = _algorithmChains.Length - 1; i >= 0; i--) + { + var alg = _algorithmChains[i]; + if (startWithEncrypt) + { + using var enc = alg.CreateEncryptor(); + input = enc.TransformFinalBlock(input, 0, input.Length); + } + else + { + using var dec = alg.CreateDecryptor(); + input = dec.TransformFinalBlock(input, 0, input.Length); + } + startWithEncrypt = !startWithEncrypt; + } + } + + return input; + } +} diff --git a/src/EazyDevirt/Core/Crypto/HMDecryptor.cs b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs new file mode 100644 index 0000000..33c4241 --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs @@ -0,0 +1,48 @@ +namespace EazyDevirt.Core.Crypto; + +internal sealed class HMDecryptor : HMDecryptionChain +{ + public HMDecryptor(byte[] password, long salt) : base(password, salt) + { + } + + public byte[] DecryptInstructionBlock(Stream instructionsStream) + { + // Read first 4 bytes (encrypted header containing original length) + var header = new byte[4]; + ReadBytes(instructionsStream, header, 0, 4); + + // Decrypt header to obtain original size + var decryptedHeader = DecryptBytes(header, startWithEncrypt: false); + var originalSize = ConvertInt32BytesToLittleEndian(decryptedHeader, 0); + + // Total encrypted size is aligned to 4 and includes 4-byte header + var alignedTotal = MinAlignToMultipleOf4(originalSize); + var remaining = alignedTotal - 4; + + var fullBlock = new byte[alignedTotal]; + Buffer.BlockCopy(header, 0, fullBlock, 0, 4); + + // Read remaining encrypted bytes + ReadBytes(instructionsStream, fullBlock, 4, remaining); + + // Decrypt full block then strip 4-byte header + var decrypted = DecryptBytes(fullBlock, startWithEncrypt: false); + var result = new byte[originalSize]; + Buffer.BlockCopy(decrypted, 4, result, 0, originalSize); + return result; + } + + private static void ReadBytes(Stream stream, byte[] buffer, int offset, int count) + { + var remaining = count; + while (remaining > 0) + { + var read = stream.Read(buffer, offset, remaining); + if (read <= 0) + throw new EndOfStreamException("Unexpected end of stream while reading encrypted homomorphic block."); + offset += read; + remaining -= read; + } + } +} diff --git a/src/EazyDevirt/Core/Crypto/PBKDF2.cs b/src/EazyDevirt/Core/Crypto/PBKDF2.cs new file mode 100644 index 0000000..4456102 --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/PBKDF2.cs @@ -0,0 +1,131 @@ +using System.Security.Cryptography; +using System; + +namespace EazyDevirt.Core.Crypto; + +internal sealed class PBKDF2 : DeriveBytes +{ + private static volatile bool HasError; + private DeriveBytes? _derived; + private readonly byte[] _password; + private readonly byte[] _salt; + private readonly int _iterations; + + public PBKDF2(byte[] password, byte[] salt, int iterations) + { + _password = (byte[])password.Clone(); + _salt = (byte[])salt.Clone(); + _iterations = iterations; + if (!HasError) + { + try + { + // Match sample behavior: try platform PBKDF2 (HMAC-SHA1) first. + _derived = new Rfc2898DeriveBytes(_password, _salt, _iterations); + } + catch + { + HasError = true; + } + } + if (_derived == null) + { + _derived = new PBKDF2_MD5(_password, _salt, _iterations); + } + } + + public override byte[] GetBytes(int cb) + { + byte[]? result = null; + if (!HasError) + { + try + { + result = _derived!.GetBytes(cb); + } + catch + { + HasError = true; + } + } + if (result == null) + { + _derived = new PBKDF2_MD5(_password, _salt, _iterations); + result = _derived.GetBytes(cb); + } + return result; + } + + public override void Reset() + { + throw new NotSupportedException(); + } + + // Fallback PBKDF2 implementation using HMAC-MD5 as PRF (mirrors decompiled sample PBKDF2-MD5). + private sealed class PBKDF2_MD5 : DeriveBytes + { + private readonly byte[] _password; + private readonly byte[] _salt; + private readonly int _iterations; + + public PBKDF2_MD5(byte[] password, byte[] salt, int iterations) + { + if (password == null) throw new ArgumentNullException(nameof(password)); + if (salt == null) throw new ArgumentNullException(nameof(salt)); + if (iterations < 1) throw new ArgumentException("iterationCount"); + _password = (byte[])password.Clone(); + _salt = (byte[])salt.Clone(); + _iterations = iterations; + } + + public override byte[] GetBytes(int cb) + { + if (cb < 0) throw new ArgumentOutOfRangeException(nameof(cb)); + const int dkLen = 16; // MD5 output size in bytes + int blocks = (cb + dkLen - 1) / dkLen; + byte[] output = new byte[blocks * dkLen]; + int offset = 0; + + for (int i = 1; i <= blocks; i++) + { + byte[] t = F(_password, _salt, _iterations, i); + Buffer.BlockCopy(t, 0, output, offset, dkLen); + offset += dkLen; + } + + if (cb < output.Length) + { + byte[] truncated = new byte[cb]; + Buffer.BlockCopy(output, 0, truncated, 0, cb); + return truncated; + } + return output; + } + + private static byte[] F(byte[] P, byte[] S, int c, int blockIndex) + { + using var hmac = new HMACMD5(P); + var saltBlock = new byte[S.Length + 4]; + Buffer.BlockCopy(S, 0, saltBlock, 0, S.Length); + // PBKDF2 uses big-endian block index + saltBlock[S.Length] = (byte)(blockIndex >> 24); + saltBlock[S.Length + 1] = (byte)(blockIndex >> 16); + saltBlock[S.Length + 2] = (byte)(blockIndex >> 8); + saltBlock[S.Length + 3] = (byte)blockIndex; + byte[] u = hmac.ComputeHash(saltBlock); + byte[] t = (byte[])u.Clone(); + for (int j = 2; j <= c; j++) + { + u = hmac.ComputeHash(u); + for (int k = 0; k < t.Length; k++) + t[k] ^= u[k]; + } + return t; + } + + public override void Reset() + { + throw new NotSupportedException(); + } + } +} diff --git a/src/EazyDevirt/Core/Crypto/Skip32Cipher.cs b/src/EazyDevirt/Core/Crypto/Skip32Cipher.cs new file mode 100644 index 0000000..6e00756 --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/Skip32Cipher.cs @@ -0,0 +1,110 @@ +using System; +using System.Security.Cryptography; + +namespace EazyDevirt.Core.Crypto; + +internal sealed class Skip32Cipher : SymmetricAlgorithm +{ + private sealed class Skip32 : IDisposable, ICryptoTransform + { + private readonly byte[] _key; + private readonly bool _isEncrypt; + + public int InputBlockSize => 4; + public int OutputBlockSize => 4; + public bool CanTransformMultipleBlocks => true; + public bool CanReuseTransform => true; + + public Skip32(byte[] key, bool isEncrypt) + { + _key = key; + _isEncrypt = isEncrypt; + } + + public void Dispose() { } + + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + if (inputCount % 4 != 0) + throw new ArgumentOutOfRangeException(nameof(inputCount), "Input count must be multiple of 4."); + for (int i = 0; i < inputCount; i += 4) + TransformOne(_key, inputBuffer, inputOffset + i, outputBuffer, outputOffset + i, _isEncrypt); + return inputCount; + } + + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + var output = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, output, 0); + return output; + } + } + + private static readonly byte[] F = new byte[256] + { + 163,215,9,131,248,72,246,244,179,33,21,120,153,177,175,249,231,45,77,138, + 206,76,202,46,82,149,217,30,78,56,68,40,10,223,2,160,23,241,96,104,18,183,122,195,233,250, + 61,83,150,132,107,186,242,99,154,25,124,174,229,245,247,22,106,162,57,182,123,15,193,147, + 129,27,238,180,26,234,208,145,47,184,85,185,218,133,63,65,191,224,90,88,128,95,102,11,216,144, + 53,213,192,167,51,6,101,105,69,0,148,86,109,152,155,118,151,252,178,194,176,254,219,32, + 225,235,214,228,221,71,74,29,66,237,158,110,73,60,205,67,39,210,7,212,222,199,103,24,137,203, + 48,31,141,198,143,170,200,116,220,201,93,92,49,164,112,136,97,44,159,13,43,135,80,130,84,100, + 38,125,3,64,52,75,28,115,209,196,253,59,204,251,127,171,230,62,91,165,173,4,35,156,20,81,34,240, + 41,121,113,126,255,140,14,226,12,239,188,114,117,111,55,161,236,211,142,98,139,134,16,232,8,119, + 17,190,146,79,36,197,50,54,157,207,243,166,187,172,94,108,169,19,87,37,181,227,189,168,58,1,5,89,42,70 + }; + + public Skip32Cipher() + { + LegalBlockSizesValue = new[] { new KeySizes(32, 32, 0) }; + LegalKeySizesValue = new[] { new KeySizes(80, 80, 0) }; + BlockSizeValue = 32; + KeySizeValue = 80; + ModeValue = CipherMode.ECB; + PaddingValue = PaddingMode.None; + } + + public Skip32Cipher(byte[] key) : this() + { + Key = key ?? throw new ArgumentNullException(nameof(key)); + } + + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? rgbIV) + => new Skip32(rgbKey, false); + + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? rgbIV) + => new Skip32(rgbKey, true); + + public override void GenerateIV() => throw new NotImplementedException(); + public override void GenerateKey() => throw new NotImplementedException(); + + private static ushort G(byte[] key, int k, ushort w) + { + byte g1 = (byte)(w >> 8); + byte g2 = (byte)w; + byte g3 = (byte)(F[g2 ^ key[4 * k % 10]] ^ g1); + byte g4 = (byte)(F[g3 ^ key[(4 * k + 1) % 10]] ^ g2); + byte g5 = (byte)(F[g4 ^ key[(4 * k + 2) % 10]] ^ g3); + byte g6 = (byte)(F[g5 ^ key[(4 * k + 3) % 10]] ^ g4); + return (ushort)((g5 << 8) + g6); + } + + private static void TransformOne(byte[] key, byte[] input, int start, byte[] output, int outputIndex, bool encrypt) + { + int step = encrypt ? 1 : -1; + int k = encrypt ? 0 : 23; + ushort wl = (ushort)((input[start] << 8) + input[start + 1]); + ushort wr = (ushort)((input[start + 2] << 8) + input[start + 3]); + for (int i = 0; i < 12; i++) + { + wr ^= (ushort)(G(key, k, wl) ^ k); + k += step; + wl ^= (ushort)(G(key, k, wr) ^ k); + k += step; + } + output[outputIndex] = (byte)(wr >> 8); + output[outputIndex + 1] = (byte)wr; + output[outputIndex + 2] = (byte)(wl >> 8); + output[outputIndex + 3] = (byte)wl; + } +} diff --git a/src/EazyDevirt/Core/Crypto/SymmetricAlgorithmChain.cs b/src/EazyDevirt/Core/Crypto/SymmetricAlgorithmChain.cs new file mode 100644 index 0000000..fe913df --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/SymmetricAlgorithmChain.cs @@ -0,0 +1,185 @@ +using System; +using System.Security.Cryptography; + +namespace EazyDevirt.Core.Crypto; + +internal sealed class SymmetricAlgorithmChain : SymmetricAlgorithm +{ + private sealed class XorTransform : IDisposable, ICryptoTransform + { + private readonly byte[] _key; + private readonly byte[] _iv; + private readonly SymmetricAlgorithm[] _algs; + private ICryptoTransform[]? _transforms; + private readonly bool _isEncryption; + private readonly int _blockSize; + + public int InputBlockSize => _blockSize; + public int OutputBlockSize => _blockSize; + public bool CanTransformMultipleBlocks => true; + public bool CanReuseTransform => true; + + public XorTransform(SymmetricAlgorithm[] algorithms, byte[] key, byte[] iv, bool isEncryption) + { + _key = key; + _iv = iv; + _algs = algorithms; + _isEncryption = isEncryption; + _blockSize = algorithms[^1].BlockSize / 8; + } + + public void Dispose() + { + if (_transforms != null) + { + foreach (var t in _transforms) + t?.Dispose(); + _transforms = null; + } + } + + private void EnsureTransforms() + { + if (_transforms != null) return; + var n = _algs.Length; + var arr = new ICryptoTransform[n]; + var offset = 0; + for (int i = 0; i < n; i++) + { + var alg = _algs[i]; + var keySizeBytes = alg.KeySize / 8; + var key = new byte[keySizeBytes]; + Buffer.BlockCopy(_key, offset, key, 0, keySizeBytes); + offset += keySizeBytes; + var iv = new byte[alg.BlockSize / 8]; + var t = _isEncryption ? alg.CreateEncryptor(key, iv) : alg.CreateDecryptor(key, iv); + arr[i] = t; + } + + _transforms = arr; + } + + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + var output = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, output, 0); + return output; + } + + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + Buffer.BlockCopy(inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); + EnsureTransforms(); + if (_isEncryption) + Encrypt(outputBuffer, outputOffset, inputCount); + else + Decrypt(outputBuffer, outputOffset, inputCount); + return inputCount; + } + + private void Encrypt(byte[] buffer, int offset, int count) + { + var iv = new byte[_iv.Length]; + Buffer.BlockCopy(_iv, 0, iv, 0, iv.Length); + var pos = 0; + foreach (var t in _transforms!) + { + var bs = t.InputBlockSize; + var len = (count - pos) & ~(bs - 1); + var end = pos + len; + for (int j = pos; j < end; j += bs) + { + var p = j + offset; + Xor(buffer, p, iv, 0, bs); + t.TransformBlock(buffer, p, bs, buffer, p); + Buffer.BlockCopy(buffer, p, iv, 0, bs); + } + pos = end; + if (end == count) break; + } + } + + private void Decrypt(byte[] buffer, int offset, int count) + { + var iv = new byte[_iv.Length]; + Buffer.BlockCopy(_iv, 0, iv, 0, iv.Length); + var tmp = new byte[iv.Length]; + var pos = 0; + foreach (var t in _transforms!) + { + var bs = t.InputBlockSize; + var len = (count - pos) & ~(bs - 1); + var end = pos + len; + for (int j = pos; j < end; j += bs) + { + var p = j + offset; + Buffer.BlockCopy(buffer, p, tmp, 0, bs); + t.TransformBlock(buffer, p, bs, buffer, p); + Xor(buffer, p, iv, 0, bs); + Buffer.BlockCopy(tmp, 0, iv, 0, bs); + } + pos = end; + if (end == count) break; + } + } + + private static void Xor(byte[] buffer, int offset, byte[] iv, int ivOffset, int count) + { + for (int i = 0; i < count; i++) + buffer[offset + i] ^= iv[ivOffset + i]; + } + } + + private static class SymmetricAlgorithmComparer + { + public static int CompareBlockSize(SymmetricAlgorithm a, SymmetricAlgorithm b) => b.BlockSize.CompareTo(a.BlockSize); + } + + private readonly SymmetricAlgorithm[] _algs; + private readonly int _ivSize; + + public override byte[] IV + { + get => base.IV; + set => IVValue = (byte[])value.Clone(); + } + + public SymmetricAlgorithmChain(params SymmetricAlgorithm[] algorithms) + { + algorithms = (SymmetricAlgorithm[])algorithms.Clone(); + Array.Sort(algorithms, SymmetricAlgorithmComparer.CompareBlockSize); + _algs = algorithms; + var totalKeyBits = 0; + foreach (var alg in algorithms) + { + totalKeyBits += alg.KeySize; + alg.Mode = CipherMode.ECB; + alg.Padding = PaddingMode.None; + } + + BlockSizeValue = algorithms[^1].BlockSize; + LegalBlockSizesValue = new[] { new KeySizes(BlockSizeValue, BlockSizeValue, 0) }; + KeySizeValue = totalKeyBits; + LegalKeySizesValue = new[] { new KeySizes(totalKeyBits, totalKeyBits, 0) }; + _ivSize = algorithms[0].BlockSize; + Mode = CipherMode.ECB; + Padding = PaddingMode.None; + } + + public int GetIVSize() => _ivSize; + + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] rgbIV) => CreateXorTransform(rgbKey, rgbIV, false); + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] rgbIV) => CreateXorTransform(rgbKey, rgbIV, true); + + private ICryptoTransform CreateXorTransform(byte[] rgbKey, byte[] rgbIv, bool isEncryption) + { + if (rgbKey.Length * 8 != KeySize) + throw new ArgumentException("Invalid key size."); + if (rgbIv.Length * 8 != GetIVSize()) + throw new ArgumentException("Invalid IV size."); + return new XorTransform(_algs, rgbKey, rgbIv, isEncryption); + } + + public override void GenerateIV() => throw new NotSupportedException(); + public override void GenerateKey() => throw new NotSupportedException(); +} diff --git a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs index e277da3..b76bf98 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs @@ -1,72 +1,104 @@ -namespace EazyDevirt.Devirtualization.Options; +using System.Collections.Generic; + +namespace EazyDevirt.Devirtualization.Options; internal record DevirtualizationOptions { #pragma warning disable CS8618 - /// - /// Target assembly info - /// - public FileInfo Assembly { get; init; } + /// + /// Target assembly info + /// + public FileInfo Assembly { get; init; } - /// - /// Path of output directory - /// - public DirectoryInfo OutputPath { get; init; } + /// + /// Path of output directory + /// + public DirectoryInfo OutputPath { get; init; } #pragma warning restore CS8618 - /// - /// Verbosity level - /// - public int Verbosity { get; init; } + /// + /// Verbosity level + /// + public int Verbosity { get; init; } - /// - /// Shows useful debug information - /// - public bool Verbose => Verbosity >= 1; + /// + /// Shows useful debug information + /// + public bool Verbose => Verbosity >= 1; - /// - /// Shows more verbose information - /// - public bool VeryVerbose => Verbosity > 1; + /// + /// Shows more verbose information + /// + public bool VeryVerbose => Verbosity > 1; - /// - /// Shows even more verbose information - /// - public bool VeryVeryVerbose => Verbosity > 2; + /// + /// Shows even more verbose information + /// + public bool VeryVeryVerbose => Verbosity > 2; - /// - /// Preserves all metadata tokens - /// - public bool PreserveAll { get; init; } + /// + /// Preserves all metadata tokens + /// + public bool PreserveAll { get; init; } - /// - /// Don't verify labels or compute max stack for devirtualized methods - /// - public bool NoVerify { get; init; } + /// + /// Don't verify labels or compute max stack for devirtualized methods + /// + public bool NoVerify { get; init; } - /// - /// Keeps all obfuscator types - /// - public bool KeepTypes { get; init; } + /// + /// Keeps all obfuscator types + /// + public bool KeepTypes { get; init; } - /// - /// Save output even if devirtualization fails - /// - public bool SaveAnyway { get; init; } + /// + /// Save output even if devirtualization fails + /// + public bool SaveAnyway { get; init; } - /// - /// Only save successfully devirtualized methods - /// - /// - /// This only matters if you're using the Save Anyway option - /// - public bool OnlySaveDevirted { get; init; } + /// + /// Only save successfully devirtualized methods + /// + /// + /// This only matters if you're using the Save Anyway option + /// + public bool OnlySaveDevirted { get; init; } - /// - /// Require dependencies when resolving generic methods - /// - /// - /// If this is disabled, methods utilizing generics (type or method args) may not have proper signatures if dependencies aren't able to be resolved - /// - public bool RequireDepsForGenericMethods { get; init; } -} \ No newline at end of file + /// + /// Require dependencies when resolving generic methods + /// + /// + /// If this is disabled, methods utilizing generics (type or method args) may not have proper signatures if dependencies aren't able to be resolved + /// + public bool RequireDepsForGenericMethods { get; init; } + + /// + /// Dictionary of homomorphic encryption password sequences keyed by method metadata token (mdtoken). + /// Provided via CLI as "--hm-pass mdtoken[:order]:type:value" (repeatable). If order is omitted, + /// passwords are appended in the order they are provided on the command line. When order is provided, + /// it is treated as a 1-based position, and passwords are consumed in ascending order per method. + /// Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. + /// + public Dictionary> HmPasswords { get; init; } = new(); +} + +/// +/// Numeric type kinds allowed for HM password values. +/// +internal enum NumericKind +{ + SByte, + Byte, + Int16, + UInt16, + Int32, + UInt32, + Int64, + UInt64, + String +} + +/// +/// Represents a typed HM password value and its precomputed big-endian bytes. +/// +internal sealed record HmPasswordEntry(NumericKind Kind, string Value, byte[] Bytes, int Order); \ No newline at end of file diff --git a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs index 226c524..4ec9161 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs @@ -1,5 +1,7 @@ -using System.CommandLine; +using System.CommandLine; using System.CommandLine.Binding; +using System.Globalization; +using System.Text; namespace EazyDevirt.Devirtualization.Options; @@ -14,10 +16,11 @@ internal class DevirtualizationOptionsBinder : BinderBase _saveAnywayOption; private readonly Option _onlySaveDevirtedOption; private readonly Option _requireDepsForGenericMethods; + private readonly Option _hmPasswordsOption; public DevirtualizationOptionsBinder(Argument assemblyArgument, Argument outputPathArgument, Option verbosityOption, Option preserveAllOption, Option noVerifyOption, Option keepTypesOption, Option saveAnywayOption, - Option onlySaveDevirtedOption, Option requireDepsForGenericMethods) + Option onlySaveDevirtedOption, Option requireDepsForGenericMethods, Option hmPasswordsOption) { _assemblyArgument = assemblyArgument; _outputPathArgument = outputPathArgument; @@ -28,6 +31,41 @@ public DevirtualizationOptionsBinder(Argument assemblyArgument, Argume _saveAnywayOption = saveAnywayOption; _onlySaveDevirtedOption = onlySaveDevirtedOption; _requireDepsForGenericMethods = requireDepsForGenericMethods; + _hmPasswordsOption = hmPasswordsOption; + } + + private static bool TryUnquote(string s, out string unquoted) + { + unquoted = string.Empty; + if (string.IsNullOrEmpty(s) || s.Length < 2) + return false; + + // Only double-quoted strings are allowed. + if (!(s[0] == '"' && s[^1] == '"')) + return false; + + var inner = s.Substring(1, s.Length - 2); + var sb = new StringBuilder(inner.Length); + for (int i = 0; i < inner.Length; i++) + { + var c = inner[i]; + if (c == '\\') + { + if (i + 1 >= inner.Length) return false; // trailing backslash is invalid + var n = inner[i + 1]; + if (n == '"' || n == '\\') + { + sb.Append(n); + i++; + continue; + } + // Disallow other escapes + return false; + } + sb.Append(c); + } + unquoted = sb.ToString(); + return true; } protected override DevirtualizationOptions GetBoundValue(BindingContext bindingContext) => @@ -41,6 +79,299 @@ protected override DevirtualizationOptions GetBoundValue(BindingContext bindingC KeepTypes = bindingContext.ParseResult.GetValueForOption(_keepTypesOption), SaveAnyway = bindingContext.ParseResult.GetValueForOption(_saveAnywayOption), OnlySaveDevirted = bindingContext.ParseResult.GetValueForOption(_onlySaveDevirtedOption), - RequireDepsForGenericMethods = bindingContext.ParseResult.GetValueForOption(_requireDepsForGenericMethods) + RequireDepsForGenericMethods = bindingContext.ParseResult.GetValueForOption(_requireDepsForGenericMethods), + HmPasswords = ParseHmPasswords(bindingContext) }; + + private Dictionary> ParseHmPasswords(BindingContext bindingContext) + { + // Temporary structure to retain insertion order for implicit-order entries. + var tmp = new Dictionary>(); + var entries = bindingContext.ParseResult.GetValueForOption(_hmPasswordsOption) ?? Array.Empty(); + var invalidSpecs = new List(); + foreach (var entry in entries) + { + if (string.IsNullOrWhiteSpace(entry)) + { + invalidSpecs.Add("--hm-pass: empty specification provided"); + continue; + } + + // Accept formats: + // 1) mdtoken:order:type:value + // 2) mdtoken:type:value + var parts = entry.Split(':', 4, StringSplitOptions.TrimEntries); + if (parts.Length < 2) + { + invalidSpecs.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:type:value' or 'mdtoken:order:type:value'."); + continue; + } + + var tokenStr = parts[0]; + if (!TryParseMdToken(tokenStr, out var token)) + { + invalidSpecs.Add($"--hm-pass '{entry}': invalid mdtoken '{tokenStr}'. Use hex with or without 0x, e.g. 0x060000AB."); + continue; + } + + // Ensure list and determine next seq + if (!tmp.TryGetValue(token, out var list)) + tmp[token] = list = new List<(NumericKind Kind, string Value, byte[] Bytes, int? Order, int Seq)>(); + var seq = list.Count; // 0-based insertion sequence within this token + + if (parts.Length >= 4 && TryParseOrder(parts[1], out var ord) && TryMapType(parts[2], out var kind1)) + { + var valueStr = parts[3]; + if (!TryParseTypedNumericToBigEndian(kind1, valueStr, out var bytes1)) + { + invalidSpecs.Add($"--hm-pass '{entry}': value '{valueStr}' is not valid for type '{parts[2]}'."); + continue; + } + list.Add((kind1, valueStr, bytes1, ord, seq)); + continue; + } + + if (parts.Length == 4 && TryParseOrder(parts[1], out _)) + { + invalidSpecs.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:order:type:value'."); + continue; + } + + if (parts.Length >= 3 && TryMapType(parts[1], out var kind2)) + { + var valueStr = string.Join(":", parts, 2, parts.Length - 2); + if (!TryParseTypedNumericToBigEndian(kind2, valueStr, out var bytes2)) + { + invalidSpecs.Add($"--hm-pass '{entry}': value '{valueStr}' is not valid for type '{parts[1]}'."); + continue; + } + list.Add((kind2, valueStr, bytes2, null, seq)); + continue; + } + + if (parts.Length == 3) + { + invalidSpecs.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:type:value'."); + continue; + } + + // Any other length (e.g., 1 or 2 parts) is invalid. + invalidSpecs.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:type:value'."); + } + + // Materialize final map with effective Order values (1-based consumption order). + var result = new Dictionary>(); + foreach (var kv in tmp) + { + var token = kv.Key; + var list = kv.Value; + // Sort: explicit orders ascending, then implicit in insertion order. + list.Sort((a, b) => + { + var aHas = a.Order.HasValue; + var bHas = b.Order.HasValue; + if (aHas && !bHas) return -1; + if (!aHas && bHas) return 1; + if (aHas && bHas) + return a.Order!.Value.CompareTo(b.Order!.Value); + return a.Seq.CompareTo(b.Seq); + }); + + var finalList = new List(list.Count); + for (int i = 0; i < list.Count; i++) + { + var t = list[i]; + // Effective consumption order is list index + 1 + finalList.Add(new HmPasswordEntry(t.Kind, t.Value, t.Bytes, i + 1)); + } + + result[token] = finalList; + } + + return result; + } + + private static bool TryParseOrder(string s, out int order) + { + order = 0; + if (string.IsNullOrWhiteSpace(s)) return false; + if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var o)) + return false; + if (o <= 0) return false; // enforce 1-based + order = o; + return true; + } + + private static bool TryParseMdToken(string s, out uint token) + { + token = 0; + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + s = s[2..]; + + return uint.TryParse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out token); + } + + private static bool TryMapType(string s, out NumericKind kind) + { + kind = default; + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim().ToLowerInvariant(); + switch (s) + { + case "sbyte": + case "i8": kind = NumericKind.SByte; return true; + case "byte": + case "u8": kind = NumericKind.Byte; return true; + case "short": + case "int16": + case "i16": kind = NumericKind.Int16; return true; + case "ushort": + case "uint16": + case "u16": kind = NumericKind.UInt16; return true; + case "int": + case "int32": + case "i32": kind = NumericKind.Int32; return true; + case "uint": + case "uint32": + case "u32": kind = NumericKind.UInt32; return true; + case "long": + case "int64": + case "i64": kind = NumericKind.Int64; return true; + case "ulong": + case "uint64": + case "u64": kind = NumericKind.UInt64; return true; + case "string": + case "str": kind = NumericKind.String; return true; + default: return false; + } + } + + private static bool TryParseTypedNumericToBigEndian(NumericKind kind, string s, out byte[] bytes) + { + bytes = Array.Empty(); + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + if (kind == NumericKind.String) + { + if (!TryUnquote(s, out var unquoted)) + return false; + bytes = Encoding.Unicode.GetBytes(unquoted); + return true; + } + + bool isHex = s.StartsWith("0x", StringComparison.OrdinalIgnoreCase); + if (isHex) + { + var hex = s[2..]; + if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) + return false; + switch (kind) + { + case NumericKind.SByte: + { + var v = unchecked((sbyte)u); + bytes = new[] { unchecked((byte)v) }; + return true; + } + case NumericKind.Byte: + { + var v = unchecked((byte)u); + bytes = new[] { v }; + return true; + } + case NumericKind.Int16: + { + var v = unchecked((short)u); + var uv = unchecked((ushort)v); + bytes = new[] { (byte)(uv >> 8), (byte)uv }; + return true; + } + case NumericKind.UInt16: + { + var v = unchecked((ushort)u); + bytes = new[] { (byte)(v >> 8), (byte)v }; + return true; + } + case NumericKind.Int32: + { + var v = unchecked((int)u); + var uv = unchecked((uint)v); + bytes = new[] { (byte)(uv >> 24), (byte)(uv >> 16), (byte)(uv >> 8), (byte)uv }; + return true; + } + case NumericKind.UInt32: + { + var v = unchecked((uint)u); + bytes = new[] { (byte)(v >> 24), (byte)(v >> 16), (byte)(v >> 8), (byte)v }; + return true; + } + case NumericKind.Int64: + { + var v = unchecked((long)u); + var uv = unchecked((ulong)v); + bytes = new[] + { + (byte)(uv >> 56), (byte)(uv >> 48), (byte)(uv >> 40), (byte)(uv >> 32), + (byte)(uv >> 24), (byte)(uv >> 16), (byte)(uv >> 8), (byte)uv + }; + return true; + } + case NumericKind.UInt64: + { + var v = unchecked((ulong)u); + bytes = new[] + { + (byte)(v >> 56), (byte)(v >> 48), (byte)(v >> 40), (byte)(v >> 32), + (byte)(v >> 24), (byte)(v >> 16), (byte)(v >> 8), (byte)v + }; + return true; + } + default: + return false; + } + } + else + { + switch (kind) + { + case NumericKind.SByte: + if (!sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) return false; + bytes = new[] { unchecked((byte)sb) }; return true; + case NumericKind.Byte: + if (!byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) return false; + bytes = new[] { b }; return true; + case NumericKind.Int16: + if (!short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh)) return false; + { + var u = unchecked((ushort)sh); + bytes = new[] { (byte)(u >> 8), (byte)u }; return true; + } + case NumericKind.UInt16: + if (!ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush)) return false; + bytes = new[] { (byte)(ush >> 8), (byte)ush }; return true; + case NumericKind.Int32: + if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32)) return false; + { + var u = unchecked((uint)i32); + bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; return true; + } + case NumericKind.UInt32: + if (!uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui32)) return false; + bytes = new[] { (byte)(ui32 >> 24), (byte)(ui32 >> 16), (byte)(ui32 >> 8), (byte)ui32 }; return true; + case NumericKind.Int64: + if (!long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64)) return false; + { + var u = unchecked((ulong)i64); + bytes = new[] { (byte)(u >> 56), (byte)(u >> 48), (byte)(u >> 40), (byte)(u >> 32), (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; return true; + } + case NumericKind.UInt64: + if (!ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui64)) return false; + bytes = new[] { (byte)(ui64 >> 56), (byte)(ui64 >> 48), (byte)(ui64 >> 40), (byte)(ui64 >> 32), (byte)(ui64 >> 24), (byte)(ui64 >> 16), (byte)(ui64 >> 8), (byte)ui64 }; return true; + default: + return false; + } + } + } } \ No newline at end of file diff --git a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs index 8a53f40..b5e5f70 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -1,8 +1,12 @@ -using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Code.Cil; using AsmResolver.PE.DotNet.Cil; using EazyDevirt.Core.Abstractions; using EazyDevirt.Core.Architecture; +using EazyDevirt.Core.Crypto; using EazyDevirt.Core.IO; +using System.Globalization; +using System.Text; +using EazyDevirt.Devirtualization.Options; namespace EazyDevirt.Devirtualization.Pipeline; @@ -10,51 +14,51 @@ internal class MethodDevirtualizer : StageBase { private CryptoStreamV3 VMStream { get; set; } private VMBinaryReader VMStreamReader { get; set; } - + private VMBinaryReader _currentReader; + private readonly Stack _readerStack = new(); + private readonly Dictionary _hmPasswordIndices = new(); + private Resolver Resolver { get; set; } - + public override bool Run() { if (!Init()) return false; - + VMStream = new CryptoStreamV3(Ctx.VMStream, Ctx.MethodCryptoKey, true); VMStreamReader = new VMBinaryReader(VMStream); - + _currentReader = VMStreamReader; + Resolver = new Resolver(Ctx); foreach (var vmMethod in Ctx.VMMethods) { VMStream.Seek(vmMethod.MethodKey, SeekOrigin.Begin); ReadVMMethod(vmMethod); - + if (Ctx.Options.VeryVerbose) Ctx.Console.Info(vmMethod); } - + VMStreamReader.Dispose(); return true; } - + private void ReadVMMethod(VMMethod vmMethod) { vmMethod.MethodInfo = new VMMethodInfo(VMStreamReader); ReadExceptionHandlers(vmMethod); - + vmMethod.MethodInfo.DeclaringType = Resolver.ResolveType(vmMethod.MethodInfo.VMDeclaringType)!; vmMethod.MethodInfo.ReturnType = Resolver.ResolveType(vmMethod.MethodInfo.VMReturnType)!; - + ResolveLocalsAndParameters(vmMethod); ReadInstructions(vmMethod); - // homomorphic encryption is not supported currently - if (!vmMethod.SuccessfullyDevirtualized && (!Ctx.Options.SaveAnyway || Ctx.Options.OnlySaveDevirted)) - return; - // these need all instructions to be successfully devirtualized to work ResolveBranchTargets(vmMethod); ResolveExceptionHandlers(vmMethod); - + // recompile method vmMethod.Parent.CilMethodBody!.LocalVariables.Clear(); vmMethod.Locals.ForEach(x => vmMethod.Parent.CilMethodBody.LocalVariables.Add(x)); @@ -73,7 +77,7 @@ private void ReadVMMethod(VMMethod vmMethod) vmMethod.Parent.CilMethodBody!.VerifyLabels(false); } } - + private void ReadExceptionHandlers(VMMethod vmMethod) { vmMethod.VMExceptionHandlers = new List(VMStreamReader.ReadInt16()); @@ -85,7 +89,7 @@ private void ReadExceptionHandlers(VMMethod vmMethod) ? second.TryLength.CompareTo(first.TryLength) : first.TryStart.CompareTo(second.TryStart)); } - + private void ResolveLocalsAndParameters(VMMethod vmMethod) { vmMethod.Locals = new List(); @@ -97,28 +101,41 @@ private void ResolveLocalsAndParameters(VMMethod vmMethod) // if (Ctx.Options.VeryVeryVerbose) // Ctx.Console.Info($"[{vmMethod.MethodInfo.Name}] Local: {type.Name}"); } - + // the parameters should already be the correct types and in the correct order so we don't need to resolve those } private void ReadInstructions(VMMethod vmMethod) { vmMethod.Instructions = new List(); - vmMethod.CodeSize = VMStreamReader.ReadInt32(); + vmMethod.CodeSize = _currentReader.ReadInt32(); vmMethod.InitialCodeStreamPosition = VMStream.Position; vmMethod.SuccessfullyDevirtualized = true; + vmMethod.VmToCilOffsetMap = new Dictionary(); + var cilOffset = 0; var finalPosition = VMStream.Position + vmMethod.CodeSize; while (VMStream.Position < finalPosition) { - vmMethod.CodePosition = vmMethod.CodeSize - (finalPosition - VMStream.Position); - var virtualOpCode = VMStreamReader.ReadInt32Special(); + if (vmMethod.HasHomomorphicEncryption && vmMethod.HMEndPositionStack.TryPeek(out var endPosition)) + vmMethod.CurrentVirtualOffset = (uint)(endPosition - (_currentReader.BaseStream.Length - _currentReader.BaseStream.Position) + vmMethod.HMEndPositionStack.Count * 8 - 4); + else + vmMethod.CurrentVirtualOffset = (uint)(vmMethod.CodeSize - (finalPosition - VMStream.Position)); + + var virtualOpCode = _currentReader.ReadInt32Special(); var vmOpCode = Ctx.PatternMatcher.GetOpCodeValue(virtualOpCode); if (!vmOpCode.HasVirtualCode) { if (Ctx.Options.VeryVerbose) Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Instruction {vmMethod.Instructions.Count}, VM opcode [{virtualOpCode}] not found!"); - - vmMethod.Instructions.Add(new CilInstruction(CilOpCodes.Nop)); + + var vmStart = (uint)vmMethod.CurrentVirtualOffset; + var nop = new CilInstruction(CilOpCodes.Nop) + { + Offset = cilOffset + }; + vmMethod.VmToCilOffsetMap[vmStart] = cilOffset; + vmMethod.Instructions.Add(nop); + cilOffset += nop.Size; continue; } @@ -134,13 +151,13 @@ private void ReadInstructions(VMMethod vmMethod) { vmOpCode.CilOpCode = ResolveSpecialCilOpCode(vmOpCode, vmMethod); if (vmOpCode.CilOpCode != null && Ctx.Options.VeryVerbose) - Ctx.Console.InfoStr($"Resolved special opcode {vmOpCode.SpecialOpCode.ToString()!} to CIL opcode {vmOpCode.CilOpCode.ToString()}", vmOpCode.SerializedDelegateMethod.MetadataToken); + Ctx.Console.InfoStr($"Resolved special opcode {vmOpCode.SpecialOpCode.ToString()!} to CIL opcode {vmOpCode.CilOpCode.ToString()}", vmMethod.Parent.MetadataToken); } var operand = vmOpCode.IsSpecial ? ReadSpecialOperand(vmOpCode, vmMethod) : ReadOperand(vmOpCode, vmMethod); if (vmOpCode.CilOpCode != null) { - // Log these for now since they're special cases. + // Log these for now since they're special cases. if (vmOpCode.CilOpCode.Value.Mnemonic.StartsWith("stind")) Ctx.Console.Warning($"Placing stind instruction at #{vmMethod.Instructions.Count}"); else if (vmOpCode.SpecialOpCode == SpecialOpCodes.NoBody) @@ -151,69 +168,54 @@ private void ReadInstructions(VMMethod vmMethod) var instruction = new CilInstruction(vmOpCode.CilOpCode.Value, operand); + var vmStart = (uint)vmMethod.CurrentVirtualOffset; + instruction.Offset = cilOffset; + vmMethod.VmToCilOffsetMap[vmStart] = cilOffset; vmMethod.Instructions.Add(instruction); + cilOffset += instruction.Size; } } - - if (vmMethod.HasHomomorphicEncryption) - vmMethod.SuccessfullyDevirtualized = false; } - private Dictionary GetVirtualOffsets(VMMethod vmMethod) - { - var virtualOffsets = new Dictionary(vmMethod.Instructions.Count) - { - { 0, 0 } - }; - var lastCilOffset = 0; - var lastOffset = 0u; - foreach (var ins in vmMethod.Instructions) - { - if (ins.OpCode == CilOpCodes.Switch) - { - var offsetsLength = (ins.Operand as Array)!.Length; - lastOffset += (uint)(4 * offsetsLength + 8); - lastCilOffset += ins.OpCode.Size + 4 + 4 * offsetsLength; - } - else - { - lastOffset += (uint)(ins.OpCode.OperandType == CilOperandType.ShortInlineBrTarget - ? 8 - : ins.Size - ins.OpCode.Size + 4); - lastCilOffset += ins.Size; - } - - virtualOffsets.Add(lastOffset, lastCilOffset); - } - - return virtualOffsets; - } - private void ResolveBranchTargets(VMMethod vmMethod) { - var virtualOffsets = GetVirtualOffsets(vmMethod); + // Reuse precomputed VM -> CIL offset map. + var vmToCil = vmMethod.VmToCilOffsetMap; for (var i = 0; i < vmMethod.Instructions.Count; i++) { var ins = vmMethod.Instructions[i]; - ins.Offset = virtualOffsets[virtualOffsets.Keys.ToArray()[i]]; switch (ins.OpCode.OperandType) { case CilOperandType.InlineBrTarget: case CilOperandType.ShortInlineBrTarget: - ins.Operand = vmMethod.SuccessfullyDevirtualized - ? new CilOffsetLabel(virtualOffsets[(uint)ins.Operand!]) - : new CilOffsetLabel(0); + { + uint vmTarget; + if (ins.Operand is uint u) vmTarget = u; + else if (ins.Operand is int si) vmTarget = unchecked((uint)si); + else { ins.Operand = new CilOffsetLabel(0); break; } + + if (vmMethod.SuccessfullyDevirtualized && vmToCil.TryGetValue(vmTarget, out var targetCil)) + ins.Operand = new CilOffsetLabel(targetCil); + else + ins.Operand = new CilOffsetLabel(0); break; + } case CilOperandType.InlineSwitch: - var offsets = ins.Operand as uint[]; - var labels = new ICilLabel[offsets!.Length]; + { + if (ins.Operand is not int[] offsets) break; + var labels = new ICilLabel[offsets.Length]; for (var x = 0; x < offsets.Length; x++) - labels[x] = vmMethod.SuccessfullyDevirtualized - ? new CilOffsetLabel(virtualOffsets[offsets[x]]) - : new CilOffsetLabel(0); + { + var vmTarget = unchecked((uint)offsets[x]); + if (vmMethod.SuccessfullyDevirtualized && vmToCil.TryGetValue(vmTarget, out var targetCil)) + labels[x] = new CilOffsetLabel(targetCil); + else + labels[x] = new CilOffsetLabel(0); + } ins.Operand = labels; break; + } } } } @@ -223,7 +225,8 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) vmMethod.ExceptionHandlers = new List(); if (!vmMethod.SuccessfullyDevirtualized) return; - var virtualOffsets = GetVirtualOffsets(vmMethod); + // Reuse precomputed VM -> CIL offset map. + var vmToCil = vmMethod.VmToCilOffsetMap; foreach (var vmExceptionHandler in vmMethod.VMExceptionHandlers) { var exceptionHandler = new CilExceptionHandler @@ -232,23 +235,23 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) ExceptionType = vmExceptionHandler.HandlerType == CilExceptionHandlerType.Exception ? Resolver.ResolveType(vmExceptionHandler.CatchType) : null }; - var handlerStart = vmMethod.Instructions.GetByOffset(virtualOffsets[vmExceptionHandler.HandlerStart]); + var handlerStart = vmMethod.Instructions.GetByOffset(vmToCil[vmExceptionHandler.HandlerStart]); exceptionHandler.HandlerStart = handlerStart?.CreateLabel(); // HandlerEnd is not explicitly defined, and we don't have a length, so we need to find it ourselves - var handlerEndIndex = vmMethod.Instructions.GetIndexByOffset(virtualOffsets[vmExceptionHandler.HandlerStart]); + var handlerEndIndex = vmMethod.Instructions.GetIndexByOffset(vmToCil[vmExceptionHandler.HandlerStart]); var foundHandlerEnd = false; while (!foundHandlerEnd && vmMethod.Instructions.Count - 1 > handlerEndIndex) { var possibleHandlerEnd = vmMethod.Instructions[handlerEndIndex]; - + // if there is a branch, skip past it to ensure the correct HandlerEnd is found if (possibleHandlerEnd.IsBranch() && possibleHandlerEnd.OpCode.Code is not (CilCode.Leave or CilCode.Leave_S)) { handlerEndIndex = vmMethod.Instructions.GetIndexByOffset(((ICilLabel)possibleHandlerEnd.Operand!).Offset); continue; } - + switch (possibleHandlerEnd.OpCode.Code) { case CilCode.Endfilter: @@ -282,18 +285,18 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) exceptionHandler.HandlerEnd = vmMethod.Instructions[handlerEndIndex].CreateLabel(); - exceptionHandler.TryStart = vmMethod.Instructions.GetByOffset(virtualOffsets[vmExceptionHandler.TryStart])?.CreateLabel(); + exceptionHandler.TryStart = vmMethod.Instructions.GetByOffset(vmToCil[vmExceptionHandler.TryStart])?.CreateLabel(); // TryEnd is equal to TryStart + TryLength + 1 var tryEndIndex = vmMethod .Instructions.GetIndexByOffset( - virtualOffsets[vmExceptionHandler.TryStart + vmExceptionHandler.TryLength]); + vmToCil[vmExceptionHandler.TryStart + vmExceptionHandler.TryLength]); exceptionHandler.TryEnd = vmMethod .Instructions[tryEndIndex + (vmMethod.Instructions.Count - 2 >= tryEndIndex ? 1 : 0)].CreateLabel(); if (vmExceptionHandler.HandlerType == CilExceptionHandlerType.Filter) - exceptionHandler.FilterStart = vmMethod.Instructions.GetByOffset(virtualOffsets[vmExceptionHandler.FilterStart])?.CreateLabel(); - + exceptionHandler.FilterStart = vmMethod.Instructions.GetByOffset(vmToCil[vmExceptionHandler.FilterStart])?.CreateLabel(); + vmMethod.ExceptionHandlers.Add(exceptionHandler); } } @@ -301,18 +304,18 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) private object? ReadOperand(VMOpCode vmOpCode, VMMethod vmMethod) => vmOpCode.CilOperandType switch // maybe switch this to vmOpCode.CilOpCode.OperandType and add more handlers { - CilOperandType.InlineI => VMStreamReader.ReadInt32Special(), - CilOperandType.ShortInlineI => VMStreamReader.ReadSByte(), - CilOperandType.InlineI8 => VMStreamReader.ReadInt64(), - CilOperandType.InlineR => VMStreamReader.ReadDouble(), - CilOperandType.ShortInlineR => VMStreamReader.ReadSingle(), - CilOperandType.InlineVar => VMStreamReader.ReadUInt16(), // IsInlineArgument(vmOpCode.CilOpCode) ? GetArgument(vmMethod, VMStreamReader.ReadUInt16()) : GetLocal(vmMethod, VMStreamReader.ReadUInt16()), - CilOperandType.ShortInlineVar => VMStreamReader.ReadByte(), // IsInlineArgument(vmOpCode.CilOpCode) ? GetArgument(vmMethod, VMStreamReader.ReadByte()) : GetLocal(vmMethod, VMStreamReader.ReadByte()), + CilOperandType.InlineI => _currentReader.ReadInt32Special(), + CilOperandType.ShortInlineI => _currentReader.ReadSByte(), + CilOperandType.InlineI8 => _currentReader.ReadInt64(), + CilOperandType.InlineR => _currentReader.ReadDouble(), + CilOperandType.ShortInlineR => _currentReader.ReadSingle(), + CilOperandType.InlineVar => _currentReader.ReadUInt16(), // IsInlineArgument(vmOpCode.CilOpCode) ? GetArgument(vmMethod, _currentReader.ReadUInt16()) : GetLocal(vmMethod, _currentReader.ReadUInt16()), + CilOperandType.ShortInlineVar => _currentReader.ReadByte(), // IsInlineArgument(vmOpCode.CilOpCode) ? GetArgument(vmMethod, _currentReader.ReadByte()) : GetLocal(vmMethod, _currentReader.ReadByte()), CilOperandType.InlineTok => ReadInlineTok(vmOpCode), CilOperandType.InlineSwitch => ReadInlineSwitch(), - CilOperandType.InlineBrTarget => VMStreamReader.ReadUInt32(), - CilOperandType.InlineArgument => VMStreamReader.ReadUInt16(), // GetArgument(vmMethod, VMStreamReader.ReadUInt16()), // this doesn't seem to be used, might not be correct - CilOperandType.ShortInlineArgument => VMStreamReader.ReadByte(), // GetArgument(vmMethod, VMStreamReader.ReadByte()), // this doesn't seem to be used, might not be correct + CilOperandType.InlineBrTarget => _currentReader.ReadUInt32(), + CilOperandType.InlineArgument => _currentReader.ReadUInt16(), // GetArgument(vmMethod, _currentReader.ReadUInt16()), // this doesn't seem to be used, might not be correct + CilOperandType.ShortInlineArgument => _currentReader.ReadByte(), // GetArgument(vmMethod, _currentReader.ReadByte()), // this doesn't seem to be used, might not be correct CilOperandType.InlineNone => null, _ => null }; @@ -320,8 +323,9 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) private object? ReadSpecialOperand(VMOpCode vmOpCode, VMMethod vmMethod) => vmOpCode.SpecialOpCode switch { - SpecialOpCodes.EazCall => Resolver.ResolveEazCall(VMStreamReader.ReadInt32Special()), + SpecialOpCodes.EazCall => Resolver.ResolveEazCall(_currentReader.ReadInt32Special()), SpecialOpCodes.StartHomomorphic => ReadHomomorphicEncryption(vmMethod), + SpecialOpCodes.EndHomomorphic => EndHomomorphic(vmMethod), _ => null }; @@ -349,7 +353,7 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) } /// - /// Processes homomorphic encryption data into CIL instructions + /// Processes homomorphic encryption data into CIL instructions /// /// /// @@ -357,27 +361,248 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) /// private int? ReadHomomorphicEncryption(VMMethod vmMethod) { - Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Detected homomorphic encryption."); + if (!vmMethod.HasHomomorphicEncryption) + { + vmMethod.HasHomomorphicEncryption = true; + vmMethod.HMEndPositionStack = new Stack(); + } + + try + { + // Get salt from the last ldc.i8 in the devirtualized instructions + var saltIns = vmMethod.Instructions.LastOrDefault(); + if (saltIns?.OpCode.Code is not CilCode.Ldc_I8 || saltIns.Operand is not long salt) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Previous instruction (salt) is not ldc.i8 in devirtualized method body."); + vmMethod.SuccessfullyDevirtualized = false; + return null; + } + + // Resolve password from CLI or prompt. + if (!TryGetHomomorphicPassword(vmMethod.Parent.MetadataToken.ToUInt32(), out var pwdEntry)) + { + var token = vmMethod.Parent.MetadataToken.ToUInt32(); + Ctx.Options.HmPasswords.TryGetValue(token, out var existingList); + var displayOrder = (existingList?.Count ?? 0) + 1; + Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Enter homomorphic password ({displayOrder}):"); + Console.Write("Type (sbyte, byte, short, ushort, int, uint, long, ulong, string): "); + var typeInput = Console.ReadLine()?.Trim() ?? string.Empty; + Console.Write("Value (decimal, 0xHEX, or text): "); + var valueInput = Console.ReadLine() ?? string.Empty; + if (string.IsNullOrWhiteSpace(valueInput)) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] No password value provided."); + vmMethod.SuccessfullyDevirtualized = false; + return null; + } + + if (string.IsNullOrWhiteSpace(typeInput) || !TryMapType(typeInput, out var kind) || !TryParseTypedNumericToBigEndian(kind, valueInput, out var bytes)) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Invalid type or value. Type must be one of sbyte, byte, short, ushort, int, uint, long, ulong, string. Value must be decimal, 0xHEX, or text."); + vmMethod.SuccessfullyDevirtualized = false; + return null; + } + if (!Ctx.Options.HmPasswords.TryGetValue(token, out var list)) + { + list = new List(); + Ctx.Options.HmPasswords[token] = list; + } + var order = list.Count + 1; + pwdEntry = new HmPasswordEntry(kind, valueInput, bytes, order); + list.Add(pwdEntry); + _hmPasswordIndices[token] = order; // next retrieval should move past this one + } + else if (Ctx.Options.VeryVerbose) + Ctx.Console.InfoStr($"Found homomorphic password from CLI arg: {pwdEntry.Value} [{pwdEntry.Kind.ToString()}].", vmMethod.Parent.MetadataToken); + + var decryptor = new HMDecryptor(pwdEntry.Bytes, salt); + + var decrypted = decryptor.DecryptInstructionBlock(VMStream); + vmMethod.HMEndPositionStack.Push((uint)(vmMethod.CurrentVirtualOffset + decrypted.Length)); + + // Swap reader to decrypted bytes and push current for nested blocks. + _readerStack.Push(_currentReader); + _currentReader = new VMBinaryReader(new MemoryStream(decrypted)); + + if (Ctx.Options.Verbose) + Ctx.Console.Success($"[{vmMethod.Parent.MetadataToken}] Switched to decrypted instruction reader (size={decrypted.Length} bytes)."); + } + catch (Exception ex) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Homomorphic decryption failed. Is the password and its type correct? Error: {ex.Message}"); + vmMethod.SuccessfullyDevirtualized = false; + } + + return null; + } + + private int? EndHomomorphic(VMMethod vmMethod) + { + try + { + if (_readerStack.Count == 0) + { + Ctx.Console.Warning($"[{vmMethod.Parent.MetadataToken}] EndHomomorphic encountered with empty reader stack"); + return null; + } + + if (vmMethod.Instructions.LastOrDefault() is not { } indexIns || !indexIns.IsLdcI4()) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] EndHomomorphic: Previous instruction (index) is not ldc.i4"); + vmMethod.SuccessfullyDevirtualized = false; + return null; + } + + // Dispose reader to free memory. + _currentReader.Dispose(); + + vmMethod.HMEndPositionStack.Pop(); + + _currentReader = _readerStack.Pop(); + if (Ctx.Options.Verbose) + Ctx.Console.Success($"[{vmMethod.Parent.MetadataToken}] Restored previous instruction reader"); + } + catch (Exception ex) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Failed to restore reader after EndHomomorphic: {ex.Message}"); + vmMethod.SuccessfullyDevirtualized = false; + } - vmMethod.HasHomomorphicEncryption = true; return null; } - private object? ReadInlineTok(VMOpCode vmOpCode) => - vmOpCode.CilOpCode?.OperandType switch + private bool TryGetHomomorphicPassword(uint mdToken, out HmPasswordEntry pwdEntry) + { + pwdEntry = null!; + var map = Ctx.Options.HmPasswords; + if (map is null || map.Count == 0) + return false; + + if (!map.TryGetValue(mdToken, out var list) || list is null || list.Count == 0) + return false; + + var idx = 0; + if (_hmPasswordIndices.TryGetValue(mdToken, out var curr)) + idx = curr; + if (idx >= list.Count) + return false; + + pwdEntry = list[idx]; + _hmPasswordIndices[mdToken] = idx + 1; // advance to the next password for subsequent homomorphic blocks + return true; + } + + private static bool TryMapType(string s, out NumericKind kind) + { + kind = default; + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim().ToLowerInvariant(); + switch (s) { - CilOperandType.InlineString => Resolver.ResolveString(VMStreamReader.ReadInt32Special()), - _ => Resolver.ResolveToken(VMStreamReader.ReadInt32Special()) - }; + case "sbyte": + case "i8": kind = NumericKind.SByte; return true; + case "byte": + case "u8": kind = NumericKind.Byte; return true; + case "short": + case "int16": + case "i16": kind = NumericKind.Int16; return true; + case "ushort": + case "uint16": + case "u16": kind = NumericKind.UInt16; return true; + case "int": + case "int32": + case "i32": kind = NumericKind.Int32; return true; + case "uint": + case "uint32": + case "u32": kind = NumericKind.UInt32; return true; + case "long": + case "int64": + case "i64": kind = NumericKind.Int64; return true; + case "ulong": + case "uint64": + case "u64": kind = NumericKind.UInt64; return true; + case "string": + case "str": kind = NumericKind.String; return true; + default: return false; + } + } - private int[] ReadInlineSwitch() + private static bool TryParseTypedNumericToBigEndian(NumericKind kind, string s, out byte[] bytes) { - var destCount = VMStreamReader.ReadInt32Special(); + bytes = Array.Empty(); + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + if (kind == NumericKind.String) + { + bytes = Encoding.Unicode.GetBytes(s); + return true; + } + var isHex = s.StartsWith("0x", StringComparison.OrdinalIgnoreCase); + if (isHex) + { + var hex = s[2..]; + if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) + return false; + switch (kind) + { + case NumericKind.SByte: + { var v = unchecked((sbyte)u); bytes = new[] { unchecked((byte)v) }; return true; } + case NumericKind.Byte: + { var v = unchecked((byte)u); bytes = new[] { v }; return true; } + case NumericKind.Int16: + { var v = unchecked((short)u); var uv = unchecked((ushort)v); bytes = new[] { (byte)(uv >> 8), (byte)uv }; return true; } + case NumericKind.UInt16: + { var v = unchecked((ushort)u); bytes = new[] { (byte)(v >> 8), (byte)v }; return true; } + case NumericKind.Int32: + { var v = unchecked((int)u); var uv = unchecked((uint)v); bytes = new[] { (byte)(uv >> 24), (byte)(uv >> 16), (byte)(uv >> 8), (byte)uv }; return true; } + case NumericKind.UInt32: + { var v = unchecked((uint)u); bytes = new[] { (byte)(v >> 24), (byte)(v >> 16), (byte)(v >> 8), (byte)v }; return true; } + case NumericKind.Int64: + { var v = unchecked((long)u); var uv = unchecked((ulong)v); bytes = new[] { (byte)(uv >> 56), (byte)(uv >> 48), (byte)(uv >> 40), (byte)(uv >> 32), (byte)(uv >> 24), (byte)(uv >> 16), (byte)(uv >> 8), (byte)uv }; return true; } + case NumericKind.UInt64: + { var v = unchecked((ulong)u); bytes = new[] { (byte)(v >> 56), (byte)(v >> 48), (byte)(v >> 40), (byte)(v >> 32), (byte)(v >> 24), (byte)(v >> 16), (byte)(v >> 8), (byte)v }; return true; } + default: return false; + } + } + + switch (kind) + { + case NumericKind.SByte: + if (!sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) return false; bytes = new[] { unchecked((byte)sb) }; return true; + case NumericKind.Byte: + if (!byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) return false; bytes = new[] { b }; return true; + case NumericKind.Int16: + if (!short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh)) return false; { var u = unchecked((ushort)sh); bytes = new[] { (byte)(u >> 8), (byte)u }; return true; } + case NumericKind.UInt16: + if (!ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush)) return false; bytes = new[] { (byte)(ush >> 8), (byte)ush }; return true; + case NumericKind.Int32: + if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32)) return false; { var u = unchecked((uint)i32); bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; return true; } + case NumericKind.UInt32: + if (!uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui32)) return false; bytes = new[] { (byte)(ui32 >> 24), (byte)(ui32 >> 16), (byte)(ui32 >> 8), (byte)ui32 }; return true; + case NumericKind.Int64: + if (!long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64)) return false; { var u = unchecked((ulong)i64); bytes = new[] { (byte)(u >> 56), (byte)(u >> 48), (byte)(u >> 40), (byte)(u >> 32), (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; return true; } + case NumericKind.UInt64: + if (!ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui64)) return false; bytes = new[] { (byte)(ui64 >> 56), (byte)(ui64 >> 48), (byte)(ui64 >> 40), (byte)(ui64 >> 32), (byte)(ui64 >> 24), (byte)(ui64 >> 16), (byte)(ui64 >> 8), (byte)ui64 }; return true; + default: + return false; + } + } + + private object? ReadInlineTok(VMOpCode vmOpCode) => + vmOpCode.CilOpCode?.OperandType switch + { + CilOperandType.InlineString => Resolver.ResolveString(_currentReader.ReadInt32Special()), + _ => Resolver.ResolveToken(_currentReader.ReadInt32Special()) + }; + + private int[] ReadInlineSwitch() + { + var destCount = _currentReader.ReadInt32Special(); var branchDests = new int[destCount]; for (var i = 0; i < destCount; i++) - branchDests[i] = VMStreamReader.ReadInt32Special(); + branchDests[i] = _currentReader.ReadInt32Special(); return branchDests; - } + } #pragma warning disable CS8618 public MethodDevirtualizer(DevirtualizationContext ctx) : base(ctx) diff --git a/src/EazyDevirt/Program.cs b/src/EazyDevirt/Program.cs index e084246..8e176d8 100644 --- a/src/EazyDevirt/Program.cs +++ b/src/EazyDevirt/Program.cs @@ -1,6 +1,7 @@ using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Parsing; +using System.Globalization; using AsmResolver.DotNet.Builder; using AsmResolver.DotNet.Code.Cil; using EazyDevirt.Devirtualization; @@ -18,8 +19,6 @@ private static async Task Main(params string[] args) var parser = BuildParser(); await parser.InvokeAsync(args).ConfigureAwait(false); - - Console.ReadLine(); } private static void Run(DevirtualizationOptions options) @@ -93,6 +92,165 @@ private static Parser BuildParser() var requireDepsForGenerics = new Option(new[] { "--require-deps-for-generics"}, "Require dependencies when resolving generic methods for accuracy"); requireDepsForGenerics.SetDefaultValue(true); + var hmPasswordsOption = new Option(new[] {"--hm-pass"}, "Homomorphic password(s) keyed by mdtoken, supporting multiple passwords per method with optional 1-based ordering. Formats: mdtoken:order:type:value | mdtoken:type:value. Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. String values must be wrapped in double quotes (\"...\") and may contain colons; escape double quotes and backslashes with a backslash. Strings use UTF-16. Repeatable; passwords are consumed in the specified order per method.") + { + Arity = ArgumentArity.ZeroOrMore + }; + + hmPasswordsOption.AddValidator(result => + { + var entries = result.GetValueForOption(hmPasswordsOption) ?? Array.Empty(); + var errors = new List(); + foreach (var entry in entries) + { + if (string.IsNullOrWhiteSpace(entry)) + { + errors.Add("--hm-pass: empty specification provided"); + continue; + } + + var parts = entry.Split(':', 4, StringSplitOptions.TrimEntries); + if (parts.Length < 2) + { + errors.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:type:value' or 'mdtoken:order:type:value'."); + continue; + } + + var tokenStr = parts[0]; + if (!TryParseMdTokenLocal(tokenStr, out _)) + { + errors.Add($"--hm-pass '{entry}': invalid mdtoken '{tokenStr}'. Use hex with or without 0x, e.g. 0x060000AB."); + continue; + } + + // Determine whether parts[1] is order or type, and compute value span accordingly. + string normType; + int valueStartIndex; + if (TryParseOrderLocal(parts[1], out _)) + { + // Expect at least 4 parts: mdtoken:order:type:value + if (parts.Length < 4) + { + errors.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:order:type:value'."); + continue; + } + if (!TryNormalizeTypeLocal(parts[2], out normType)) + { + errors.Add($"--hm-pass '{entry}': unknown type '{parts[2]}'."); + continue; + } + valueStartIndex = 3; + } + else if (TryNormalizeTypeLocal(parts[1], out normType)) + { + // Typed-only: mdtoken:type:value + if (parts.Length < 3) + { + errors.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:type:value'."); + continue; + } + valueStartIndex = 2; + } + else + { + errors.Add($"--hm-pass '{entry}': invalid format. Expected 'mdtoken:type:value' or 'mdtoken:order:type:value'."); + continue; + } + + var valueJoined = string.Join(":", parts, valueStartIndex, parts.Length - valueStartIndex); + if (!TryValidateValueForTypeLocal(normType, valueJoined)) + { + errors.Add($"--hm-pass '{entry}': value '{valueJoined}' is not valid for type '{normType}'."); + continue; + } + } + + if (errors.Count > 0) + result.ErrorMessage = + "Invalid --hm-pass specification(s):\n" + + string.Join(Environment.NewLine, errors) + + "\nAccepted formats: mdtoken:order:type:value | mdtoken:type:value"; + + static bool TryParseMdTokenLocal(string s, out uint token) + { + token = 0; + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + s = s[2..]; + return uint.TryParse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out token); + } + + static bool TryParseOrderLocal(string s, out int order) + { + order = 0; + if (string.IsNullOrWhiteSpace(s)) return false; + if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var o)) return false; + if (o <= 0) return false; + order = o; return true; + } + + static bool TryNormalizeTypeLocal(string s, out string norm) + { + norm = string.Empty; + if (string.IsNullOrWhiteSpace(s)) return false; + switch (s.Trim().ToLowerInvariant()) + { + case "sbyte": case "i8": norm = "sbyte"; return true; + case "byte": case "u8": norm = "byte"; return true; + case "short": case "int16": case "i16": norm = "int16"; return true; + case "ushort": case "uint16": case "u16": norm = "uint16"; return true; + case "int": case "int32": case "i32": norm = "int32"; return true; + case "uint": case "uint32": case "u32": norm = "uint32"; return true; + case "long": case "int64": case "i64": norm = "int64"; return true; + case "ulong": case "uint64": case "u64": norm = "uint64"; return true; + case "string": case "str": norm = "string"; return true; + default: return false; + } + } + + static bool TryValidateValueForTypeLocal(string type, string value) + { + if (type == "string") + { + // Must be wrapped in double quotes so colons can be included safely. + if (string.IsNullOrEmpty(value) || value.Length < 2) return false; + if (!(value[0] == '"' && value[^1] == '"')) return false; + // Validate escapes inside string: only allow \" and \\ + for (int i = 1; i < value.Length - 1; i++) + { + if (value[i] == '\\') + { + if (i + 1 >= value.Length - 1) return false; // trailing backslash + var n = value[i + 1]; + if (n == '"' || n == '\\') + { i++; continue; } + return false; // disallow other escapes (e.g., \' or \n) + } + } + return true; + } + if (string.IsNullOrWhiteSpace(value)) return false; + var s = value.Trim(); + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + // For hex we accept any size and rely on unchecked casts in binder + return ulong.TryParse(s[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _); + } + return type switch + { + "sbyte" => sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "byte" => byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "int16" => short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "uint16" => ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "int32" => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "uint32" => uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "int64" => long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + "uint64" => ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _), + _ => false + }; + } + }); var rootCommand = new RootCommand("is an open-source tool that automatically restores the original IL code " + "from an assembly virtualized with Eazfuscator.NET") @@ -105,13 +263,14 @@ private static Parser BuildParser() keepTypesOption, saveAnywayOption, onlySaveDevirtedOption, - requireDepsForGenerics + requireDepsForGenerics, + hmPasswordsOption }; rootCommand.SetHandler(Run, new DevirtualizationOptionsBinder(inputArgument, outputArgument, verbosityOption, preserveAllOption, noVerifyOption, keepTypesOption, saveAnywayOption, onlySaveDevirtedOption, - requireDepsForGenerics)); + requireDepsForGenerics, hmPasswordsOption)); return new CommandLineBuilder(rootCommand) .UseDefaults()