From 628c493c79ddd6751959ecd1404695b2c9ec9c4f Mon Sep 17 00:00:00 2001 From: puff <33184334+puff@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:24:21 -0400 Subject: [PATCH 1/6] Initial homomorphic decryption --- src/EazyDevirt/Core/Crypto/HMDecryptor.cs | 51 +++ .../Core/Crypto/HMEncryptionChain.cs | 108 +++++ src/EazyDevirt/Core/Crypto/PBKDF2.cs | 131 ++++++ src/EazyDevirt/Core/Crypto/Skip32Cipher.cs | 110 +++++ .../Core/Crypto/SymmetricAlgorithmChain.cs | 185 ++++++++ .../Options/DevirtualizationOptions.cs | 142 +++--- .../Options/DevirtualizationOptionsBinder.cs | 270 +++++++++++- .../Pipeline/MethodDevirtualizer.cs | 405 +++++++++++++++--- src/EazyDevirt/Program.cs | 9 +- 9 files changed, 1296 insertions(+), 115 deletions(-) create mode 100644 src/EazyDevirt/Core/Crypto/HMDecryptor.cs create mode 100644 src/EazyDevirt/Core/Crypto/HMEncryptionChain.cs create mode 100644 src/EazyDevirt/Core/Crypto/PBKDF2.cs create mode 100644 src/EazyDevirt/Core/Crypto/Skip32Cipher.cs create mode 100644 src/EazyDevirt/Core/Crypto/SymmetricAlgorithmChain.cs diff --git a/src/EazyDevirt/Core/Crypto/HMDecryptor.cs b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs new file mode 100644 index 0000000..3a2d229 --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; + +namespace EazyDevirt.Core.Crypto; + +internal sealed class HMDecryptor : HMEncryptionChain +{ + 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/HMEncryptionChain.cs b/src/EazyDevirt/Core/Crypto/HMEncryptionChain.cs new file mode 100644 index 0000000..cf384bc --- /dev/null +++ b/src/EazyDevirt/Core/Crypto/HMEncryptionChain.cs @@ -0,0 +1,108 @@ +using System.Security.Cryptography; + +namespace EazyDevirt.Core.Crypto; + +internal abstract class HMEncryptionChain +{ + private readonly SymmetricAlgorithm[] _algorithmChains; + + protected HMEncryptionChain(byte[] password, long salt) + : this(password, ConvertLongToLittleEndian(salt)) + { + } + + protected HMEncryptionChain(byte[] password, byte[] salt) + { + var pbkdf = new PBKDF2(password, salt, 1); + var array = new SymmetricAlgorithm[5]; + for (int 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/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..46db2d8 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs @@ -1,72 +1,102 @@ -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 passwords keyed by method metadata token (mdtoken). + /// Provided via CLI as "--hm-pass mdtoken:type:value" (repeatable). + /// 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); \ No newline at end of file diff --git a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs index 226c524..a1b3774 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs @@ -1,5 +1,8 @@ -using System.CommandLine; +using System; +using System.CommandLine; using System.CommandLine.Binding; +using System.Globalization; +using System.Text; namespace EazyDevirt.Devirtualization.Options; @@ -14,10 +17,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 +32,7 @@ public DevirtualizationOptionsBinder(Argument assemblyArgument, Argume _saveAnywayOption = saveAnywayOption; _onlySaveDevirtedOption = onlySaveDevirtedOption; _requireDepsForGenericMethods = requireDepsForGenericMethods; + _hmPasswordsOption = hmPasswordsOption; } protected override DevirtualizationOptions GetBoundValue(BindingContext bindingContext) => @@ -41,6 +46,265 @@ 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) + { + var dict = new Dictionary(); + var entries = bindingContext.ParseResult.GetValueForOption(_hmPasswordsOption) ?? Array.Empty(); + foreach (var entry in entries) + { + if (string.IsNullOrWhiteSpace(entry)) + continue; + // Accept formats: + // 1) mdtoken:type:value + // 2) mdtoken:value (fallback, auto-width) + var parts = entry.Split(':', 3, StringSplitOptions.TrimEntries); + if (parts.Length < 2) + continue; + + var tokenStr = parts[0]; + if (!TryParseMdToken(tokenStr, out var token)) + continue; + + if (parts.Length == 3) + { + var typeStr = parts[1]; + var valueStr = parts[2]; + if (!TryMapType(typeStr, out var kind)) + continue; + if (!TryParseTypedNumericToBigEndian(kind, valueStr, out var bytes)) + continue; + dict[token] = new HmPasswordEntry(kind, valueStr, bytes); + } + else // parts.Length == 2 -> fallback (auto-width) + { + var valueStr = parts[1]; + if (!TryParseNumericAutoWidthToBigEndian(valueStr, out var bytes, out var inferredKind)) + continue; + dict[token] = new HmPasswordEntry(inferredKind, valueStr, bytes); + } + } + + return dict; + } + + 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) + { + bytes = Encoding.Unicode.GetBytes(s); + 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; + } + } + } + + private static bool TryParseNumericAutoWidthToBigEndian(string s, out byte[] bytes, out NumericKind kind) + { + bytes = Array.Empty(); + kind = default; + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + var hex = s[2..]; + if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) + return false; + if (u <= byte.MaxValue) + { bytes = new[] { (byte)u }; kind = NumericKind.Byte; return true; } + if (u <= ushort.MaxValue) + { bytes = new[] { (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt16; return true; } + if (u <= uint.MaxValue) + { bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt32; return true; } + 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 + }; kind = NumericKind.UInt64; return true; + } + + // Decimal: try signed then unsigned to preserve width semantics + if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) + { bytes = new[] { unchecked((byte)sb) }; kind = NumericKind.SByte; return true; } + if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) + { bytes = new[] { b }; kind = NumericKind.Byte; return true; } + if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh)) + { var u = unchecked((ushort)sh); bytes = new[] { (byte)(u >> 8), (byte)u }; kind = NumericKind.Int16; return true; } + if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush)) + { bytes = new[] { (byte)(ush >> 8), (byte)ush }; kind = NumericKind.UInt16; return true; } + if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32)) + { var u = unchecked((uint)i32); bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; kind = NumericKind.Int32; return true; } + if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui32)) + { bytes = new[] { (byte)(ui32 >> 24), (byte)(ui32 >> 16), (byte)(ui32 >> 8), (byte)ui32 }; kind = NumericKind.UInt32; return true; } + if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64)) + { 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 }; kind = NumericKind.Int64; return true; } + if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui64)) + { 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 }; kind = NumericKind.UInt64; return true; } + + 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..c9ca150 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -1,8 +1,17 @@ -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; +using System.IO; +using System.Linq; +using System.Globalization; +using System.Text; +using System.Collections.Generic; +using EazyDevirt.PatternMatching.Patterns.OpCodes; +using EazyDevirt.Devirtualization.Options; namespace EazyDevirt.Devirtualization.Pipeline; @@ -10,48 +19,47 @@ internal class MethodDevirtualizer : StageBase { private CryptoStreamV3 VMStream { get; set; } private VMBinaryReader VMStreamReader { get; set; } - + private VMBinaryReader _currentReader; + private readonly Stack _readerStack = 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); @@ -64,7 +72,7 @@ private void ReadVMMethod(VMMethod vmMethod) vmMethod.Parent.CilMethodBody.Instructions.Clear(); vmMethod.Instructions.ForEach(x => vmMethod.Parent.CilMethodBody.Instructions.Add(x)); - + vmMethod.Parent.CilMethodBody!.VerifyLabelsOnBuild = false; vmMethod.Parent.CilMethodBody!.ComputeMaxStackOnBuild = false; if (vmMethod.SuccessfullyDevirtualized && !Ctx.Options.NoVerify) @@ -73,7 +81,7 @@ private void ReadVMMethod(VMMethod vmMethod) vmMethod.Parent.CilMethodBody!.VerifyLabels(false); } } - + private void ReadExceptionHandlers(VMMethod vmMethod) { vmMethod.VMExceptionHandlers = new List(VMStreamReader.ReadInt16()); @@ -85,7 +93,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,27 +105,27 @@ 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; var finalPosition = VMStream.Position + vmMethod.CodeSize; while (VMStream.Position < finalPosition) { vmMethod.CodePosition = vmMethod.CodeSize - (finalPosition - VMStream.Position); - var virtualOpCode = VMStreamReader.ReadInt32Special(); + 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)); continue; } @@ -134,13 +142,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) @@ -154,9 +162,6 @@ private void ReadInstructions(VMMethod vmMethod) vmMethod.Instructions.Add(instruction); } } - - if (vmMethod.HasHomomorphicEncryption) - vmMethod.SuccessfullyDevirtualized = false; } private Dictionary GetVirtualOffsets(VMMethod vmMethod) @@ -188,7 +193,7 @@ private Dictionary GetVirtualOffsets(VMMethod vmMethod) return virtualOffsets; } - + private void ResolveBranchTargets(VMMethod vmMethod) { var virtualOffsets = GetVirtualOffsets(vmMethod); @@ -222,7 +227,7 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) { vmMethod.ExceptionHandlers = new List(); if (!vmMethod.SuccessfullyDevirtualized) return; - + var virtualOffsets = GetVirtualOffsets(vmMethod); foreach (var vmExceptionHandler in vmMethod.VMExceptionHandlers) { @@ -241,14 +246,14 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) 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: @@ -293,7 +298,7 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) if (vmExceptionHandler.HandlerType == CilExceptionHandlerType.Filter) exceptionHandler.FilterStart = vmMethod.Instructions.GetByOffset(virtualOffsets[vmExceptionHandler.FilterStart])?.CreateLabel(); - + vmMethod.ExceptionHandlers.Add(exceptionHandler); } } @@ -301,18 +306,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 +325,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 +355,7 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) } /// - /// Processes homomorphic encryption data into CIL instructions + /// Processes homomorphic encryption data into CIL instructions /// /// /// @@ -357,27 +363,318 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) /// private int? ReadHomomorphicEncryption(VMMethod vmMethod) { - Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Detected homomorphic encryption."); - vmMethod.HasHomomorphicEncryption = true; + 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 (typed). + if (!TryGetHomomorphicPassword(vmMethod.Parent.MetadataToken.ToUInt32(), out var pwdEntry)) + { + Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Enter homomorphic password (typed):"); + Console.Write("Type (sbyte, byte, short, ushort, int, uint, long, ulong, string) [empty=auto]: "); + 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)) + { + if (!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 for string."); + vmMethod.SuccessfullyDevirtualized = false; + return null; + } + pwdEntry = new HmPasswordEntry(kind, valueInput, bytes); + } + else + { + if (!TryParseNumericAutoWidthToBigEndian(valueInput, out var bytes, out var inferredKind)) + { + Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Invalid password value. Provide an integer (decimal or 0xHEX)."); + vmMethod.SuccessfullyDevirtualized = false; + return null; + } + pwdEntry = new HmPasswordEntry(inferredKind, valueInput, bytes); + } + + // Cache for subsequent use within the same run. + Ctx.Options.HmPasswords[vmMethod.Parent.MetadataToken.ToUInt32()] = pwdEntry; + } + 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); + + // 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 object? ReadInlineTok(VMOpCode vmOpCode) => - vmOpCode.CilOpCode?.OperandType switch + private int? EndHomomorphic(VMMethod vmMethod) + { + try { - CilOperandType.InlineString => Resolver.ResolveString(VMStreamReader.ReadInt32Special()), - _ => Resolver.ResolveToken(VMStreamReader.ReadInt32Special()) - }; + if (_readerStack.Count == 0) + { + Ctx.Console.Warning($"[{vmMethod.Parent.MetadataToken}] EndHomomorphic encountered with empty reader stack."); + return null; + } + + // Dispose decrypted reader to free memory. + if (!ReferenceEquals(_currentReader, VMStreamReader)) + _currentReader.Dispose(); + + _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; + } + + return null; + } + + private bool TryGetHomomorphicPassword(uint mdToken, out HmPasswordEntry pwdEntry) + { + pwdEntry = null!; + var map = Ctx.Options.HmPasswords; + if (map is null || map.Count == 0) + return false; + + return map.TryGetValue(mdToken, out pwdEntry); + } + + private static bool TryParseNumericToBigEndian(string s, out byte[] bytes) + { + bytes = Array.Empty(); + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + + // Hex prefixed value => parse as unsigned and choose minimal width + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + var hex = s[2..]; + if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) + return false; + + if (u <= byte.MaxValue) + bytes = new[] { (byte)u }; + else if (u <= ushort.MaxValue) + bytes = new[] { (byte)(u >> 8), (byte)u }; + else if (u <= uint.MaxValue) + bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; + else + 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; + } + + // Decimal: try signed then unsigned types to preserve width semantics + if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) + { bytes = new[] { unchecked((byte)sb) }; return true; } + if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) + { bytes = new[] { b }; return true; } + if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh)) + { var u = unchecked((ushort)sh); bytes = new[] { (byte)(u >> 8), (byte)u }; return true; } + if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush)) + { bytes = new[] { (byte)(ush >> 8), (byte)ush }; return true; } + if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32)) + { var u = unchecked((uint)i32); bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; return true; } + if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui32)) + { bytes = new[] { (byte)(ui32 >> 24), (byte)(ui32 >> 16), (byte)(ui32 >> 8), (byte)ui32 }; return true; } + if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64)) + { 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; } + if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui64)) + { 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; } + + return false; + } + + 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 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; + } + } + 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; + } + } + } + + private static bool TryParseNumericAutoWidthToBigEndian(string s, out byte[] bytes, out NumericKind kind) + { + bytes = Array.Empty(); + kind = default; + if (string.IsNullOrWhiteSpace(s)) return false; + s = s.Trim(); + if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + var hex = s[2..]; + if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) + return false; + if (u <= byte.MaxValue) + { bytes = new[] { (byte)u }; kind = NumericKind.Byte; return true; } + if (u <= ushort.MaxValue) + { bytes = new[] { (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt16; return true; } + if (u <= uint.MaxValue) + { bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt32; return true; } + 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 }; kind = NumericKind.UInt64; return true; + } + if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb2)) + { bytes = new[] { unchecked((byte)sb2) }; kind = NumericKind.SByte; return true; } + if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b2)) + { bytes = new[] { b2 }; kind = NumericKind.Byte; return true; } + if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh2)) + { var u2 = unchecked((ushort)sh2); bytes = new[] { (byte)(u2 >> 8), (byte)u2 }; kind = NumericKind.Int16; return true; } + if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush2)) + { bytes = new[] { (byte)(ush2 >> 8), (byte)ush2 }; kind = NumericKind.UInt16; return true; } + if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i322)) + { var u3 = unchecked((uint)i322); bytes = new[] { (byte)(u3 >> 24), (byte)(u3 >> 16), (byte)(u3 >> 8), (byte)u3 }; kind = NumericKind.Int32; return true; } + if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui322)) + { bytes = new[] { (byte)(ui322 >> 24), (byte)(ui322 >> 16), (byte)(ui322 >> 8), (byte)ui322 }; kind = NumericKind.UInt32; return true; } + if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i642)) + { var u4 = unchecked((ulong)i642); bytes = new[] { (byte)(u4 >> 56), (byte)(u4 >> 48), (byte)(u4 >> 40), (byte)(u4 >> 32), (byte)(u4 >> 24), (byte)(u4 >> 16), (byte)(u4 >> 8), (byte)u4 }; kind = NumericKind.Int64; return true; } + if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui642)) + { bytes = new[] { (byte)(ui642 >> 56), (byte)(ui642 >> 48), (byte)(ui642 >> 40), (byte)(ui642 >> 32), (byte)(ui642 >> 24), (byte)(ui642 >> 16), (byte)(ui642 >> 8), (byte)ui642 }; kind = NumericKind.UInt64; return true; } + 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..9450484 100644 --- a/src/EazyDevirt/Program.cs +++ b/src/EazyDevirt/Program.cs @@ -93,6 +93,10 @@ 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. Format: mdtoken:type:value or mdtoken:value (auto-width, may fail). Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. String uses UTF-16. Repeatable.") + { + Arity = ArgumentArity.ZeroOrMore + }; 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 +109,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() From ad3b8498f3e406d6ed67084befa85f4f8c6ed2ce Mon Sep 17 00:00:00 2001 From: puff <33184334+puff@users.noreply.github.com> Date: Sat, 6 Sep 2025 09:21:07 -0400 Subject: [PATCH 2/6] Rename HMEncryptionChain to HMDecryptionChain --- ...ncryptionChain.cs => HMDecryptionChain.cs} | 6 ++--- src/EazyDevirt/Core/Crypto/HMDecryptor.cs | 2 +- .../Pipeline/MethodDevirtualizer.cs | 22 ++++++++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) rename src/EazyDevirt/Core/Crypto/{HMEncryptionChain.cs => HMDecryptionChain.cs} (95%) diff --git a/src/EazyDevirt/Core/Crypto/HMEncryptionChain.cs b/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs similarity index 95% rename from src/EazyDevirt/Core/Crypto/HMEncryptionChain.cs rename to src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs index cf384bc..3ebd957 100644 --- a/src/EazyDevirt/Core/Crypto/HMEncryptionChain.cs +++ b/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs @@ -2,16 +2,16 @@ namespace EazyDevirt.Core.Crypto; -internal abstract class HMEncryptionChain +internal abstract class HMDecryptionChain { private readonly SymmetricAlgorithm[] _algorithmChains; - protected HMEncryptionChain(byte[] password, long salt) + protected HMDecryptionChain(byte[] password, long salt) : this(password, ConvertLongToLittleEndian(salt)) { } - protected HMEncryptionChain(byte[] password, byte[] salt) + protected HMDecryptionChain(byte[] password, byte[] salt) { var pbkdf = new PBKDF2(password, salt, 1); var array = new SymmetricAlgorithm[5]; diff --git a/src/EazyDevirt/Core/Crypto/HMDecryptor.cs b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs index 3a2d229..a2b1dd5 100644 --- a/src/EazyDevirt/Core/Crypto/HMDecryptor.cs +++ b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs @@ -3,7 +3,7 @@ namespace EazyDevirt.Core.Crypto; -internal sealed class HMDecryptor : HMEncryptionChain +internal sealed class HMDecryptor : HMDecryptionChain { public HMDecryptor(byte[] password, long salt) : base(password, salt) { diff --git a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs index c9ca150..a0ac3ef 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -170,6 +170,7 @@ private Dictionary GetVirtualOffsets(VMMethod vmMethod) { { 0, 0 } }; + var lastCilOffset = 0; var lastOffset = 0u; foreach (var ins in vmMethod.Instructions) @@ -443,18 +444,23 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) { if (_readerStack.Count == 0) { - Ctx.Console.Warning($"[{vmMethod.Parent.MetadataToken}] EndHomomorphic encountered with empty reader stack."); + Ctx.Console.Warning($"[{vmMethod.Parent.MetadataToken}] EndHomomorphic encountered with empty reader stack"); return null; } - - // Dispose decrypted reader to free memory. - if (!ReferenceEquals(_currentReader, VMStreamReader)) - _currentReader.Dispose(); - + + 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(); + _currentReader = _readerStack.Pop(); - if (Ctx.Options.Verbose) - Ctx.Console.Success($"[{vmMethod.Parent.MetadataToken}] Restored previous instruction reader."); + Ctx.Console.Success($"[{vmMethod.Parent.MetadataToken}] Restored previous instruction reader"); } catch (Exception ex) { From 7e200aa85793f93ab4b1046e856332db94e50da6 Mon Sep 17 00:00:00 2001 From: puff <33184334+puff@users.noreply.github.com> Date: Sat, 6 Sep 2025 16:55:05 -0400 Subject: [PATCH 3/6] Fix virtual offset calculation --- src/EazyDevirt/Core/Architecture/VMMethod.cs | 17 ++- .../Pipeline/MethodDevirtualizer.cs | 114 ++++++++++-------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/src/EazyDevirt/Core/Architecture/VMMethod.cs b/src/EazyDevirt/Core/Architecture/VMMethod.cs index 24a46ca..acaf47e 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,23 @@ 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 int CurrentVirtualOffset { get; set; } + /// + /// For each instruction index, stores the VM "virtual offset" captured at decode-time. + /// + public List InstructionVirtualOffsets { get; set; } + /// + /// Mapping from VM virtual offset to CIL offset, built once during decoding. + /// + 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/Devirtualization/Pipeline/MethodDevirtualizer.cs b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs index a0ac3ef..778886b 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -115,10 +115,17 @@ private void ReadInstructions(VMMethod vmMethod) vmMethod.CodeSize = _currentReader.ReadInt32(); vmMethod.InitialCodeStreamPosition = VMStream.Position; vmMethod.SuccessfullyDevirtualized = true; + vmMethod.InstructionVirtualOffsets = new List(); + vmMethod.VmToCilOffsetMap = new Dictionary(); + var cilOffset = 0; var finalPosition = VMStream.Position + vmMethod.CodeSize; while (VMStream.Position < finalPosition) { - vmMethod.CodePosition = vmMethod.CodeSize - (finalPosition - VMStream.Position); + if (vmMethod.HasHomomorphicEncryption && vmMethod.HMEndPositionStack.TryPeek(out var endPosition)) + vmMethod.CurrentVirtualOffset = (int)(endPosition - (_currentReader.BaseStream.Length - _currentReader.BaseStream.Position)) + vmMethod.HMEndPositionStack.Count * 8 - 4; + else + vmMethod.CurrentVirtualOffset = (int)(vmMethod.CodeSize - (finalPosition - VMStream.Position)); + var virtualOpCode = _currentReader.ReadInt32Special(); var vmOpCode = Ctx.PatternMatcher.GetOpCodeValue(virtualOpCode); if (!vmOpCode.HasVirtualCode) @@ -126,7 +133,15 @@ private void ReadInstructions(VMMethod vmMethod) 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.InstructionVirtualOffsets.Add(vmStart); + vmMethod.VmToCilOffsetMap[vmStart] = cilOffset; + vmMethod.Instructions.Add(nop); + cilOffset += nop.Size; continue; } @@ -159,67 +174,55 @@ private void ReadInstructions(VMMethod vmMethod) var instruction = new CilInstruction(vmOpCode.CilOpCode.Value, operand); + var vmStart = (uint)vmMethod.CurrentVirtualOffset; + instruction.Offset = cilOffset; + vmMethod.InstructionVirtualOffsets.Add(vmStart); + vmMethod.VmToCilOffsetMap[vmStart] = cilOffset; vmMethod.Instructions.Add(instruction); + cilOffset += instruction.Size; } } } - 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; + } } } } @@ -228,8 +231,9 @@ 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 @@ -238,11 +242,11 @@ 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) { @@ -288,17 +292,17 @@ 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); } @@ -364,7 +368,12 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) /// private int? ReadHomomorphicEncryption(VMMethod vmMethod) { - vmMethod.HasHomomorphicEncryption = true; + if (!vmMethod.HasHomomorphicEncryption) + { + vmMethod.HMEndPositionStack = new Stack(); + vmMethod.HasHomomorphicEncryption = true; + } + try { // Get salt from the last ldc.i8 in the devirtualized instructions @@ -421,6 +430,7 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) var decryptor = new HMDecryptor(pwdEntry.Bytes, salt); var decrypted = decryptor.DecryptInstructionBlock(VMStream); + vmMethod.HMEndPositionStack.Push(vmMethod.CurrentVirtualOffset + decrypted.Length); // Swap reader to decrypted bytes and push current for nested blocks. _readerStack.Push(_currentReader); @@ -457,6 +467,8 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) // Dispose reader to free memory. _currentReader.Dispose(); + + vmMethod.HMEndPositionStack.Pop(); _currentReader = _readerStack.Pop(); if (Ctx.Options.Verbose) From 8f5cee2bce9989722eb47abd9dd8f8134ad42537 Mon Sep 17 00:00:00 2001 From: puff <33184334+puff@users.noreply.github.com> Date: Sat, 6 Sep 2025 17:20:01 -0400 Subject: [PATCH 4/6] Change offset variables to be uints --- src/EazyDevirt/Core/Architecture/VMMethod.cs | 10 +++------- .../Pipeline/MethodDevirtualizer.cs | 15 ++++++--------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/EazyDevirt/Core/Architecture/VMMethod.cs b/src/EazyDevirt/Core/Architecture/VMMethod.cs index acaf47e..5b45aeb 100644 --- a/src/EazyDevirt/Core/Architecture/VMMethod.cs +++ b/src/EazyDevirt/Core/Architecture/VMMethod.cs @@ -20,20 +20,16 @@ internal record VMMethod(MethodDefinition Parent, string EncodedMethodKey, long public bool SuccessfullyDevirtualized { get; set; } public bool HasHomomorphicEncryption { get; set; } public int CodeSize { get; set; } - public int CurrentVirtualOffset { get; set; } + public uint CurrentVirtualOffset { get; set; } /// - /// For each instruction index, stores the VM "virtual offset" captured at decode-time. - /// - public List InstructionVirtualOffsets { get; set; } - /// - /// Mapping from VM virtual offset to CIL offset, built once during decoding. + /// 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 Stack HMEndPositionStack { get; set; } public override string ToString() => $"Parent: {Parent.MetadataToken} | EncodedMethodKey: {EncodedMethodKey} | MethodKey: 0x{MethodKey:X} | " + diff --git a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs index 778886b..16a72cb 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -62,7 +62,7 @@ private void ReadVMMethod(VMMethod vmMethod) ResolveBranchTargets(vmMethod); ResolveExceptionHandlers(vmMethod); - + // recompile method vmMethod.Parent.CilMethodBody!.LocalVariables.Clear(); vmMethod.Locals.ForEach(x => vmMethod.Parent.CilMethodBody.LocalVariables.Add(x)); @@ -72,7 +72,7 @@ private void ReadVMMethod(VMMethod vmMethod) vmMethod.Parent.CilMethodBody.Instructions.Clear(); vmMethod.Instructions.ForEach(x => vmMethod.Parent.CilMethodBody.Instructions.Add(x)); - + vmMethod.Parent.CilMethodBody!.VerifyLabelsOnBuild = false; vmMethod.Parent.CilMethodBody!.ComputeMaxStackOnBuild = false; if (vmMethod.SuccessfullyDevirtualized && !Ctx.Options.NoVerify) @@ -115,16 +115,15 @@ private void ReadInstructions(VMMethod vmMethod) vmMethod.CodeSize = _currentReader.ReadInt32(); vmMethod.InitialCodeStreamPosition = VMStream.Position; vmMethod.SuccessfullyDevirtualized = true; - vmMethod.InstructionVirtualOffsets = new List(); vmMethod.VmToCilOffsetMap = new Dictionary(); var cilOffset = 0; var finalPosition = VMStream.Position + vmMethod.CodeSize; while (VMStream.Position < finalPosition) { if (vmMethod.HasHomomorphicEncryption && vmMethod.HMEndPositionStack.TryPeek(out var endPosition)) - vmMethod.CurrentVirtualOffset = (int)(endPosition - (_currentReader.BaseStream.Length - _currentReader.BaseStream.Position)) + vmMethod.HMEndPositionStack.Count * 8 - 4; + vmMethod.CurrentVirtualOffset = (uint)(endPosition - (_currentReader.BaseStream.Length - _currentReader.BaseStream.Position) + vmMethod.HMEndPositionStack.Count * 8 - 4); else - vmMethod.CurrentVirtualOffset = (int)(vmMethod.CodeSize - (finalPosition - VMStream.Position)); + vmMethod.CurrentVirtualOffset = (uint)(vmMethod.CodeSize - (finalPosition - VMStream.Position)); var virtualOpCode = _currentReader.ReadInt32Special(); var vmOpCode = Ctx.PatternMatcher.GetOpCodeValue(virtualOpCode); @@ -138,7 +137,6 @@ private void ReadInstructions(VMMethod vmMethod) { Offset = cilOffset }; - vmMethod.InstructionVirtualOffsets.Add(vmStart); vmMethod.VmToCilOffsetMap[vmStart] = cilOffset; vmMethod.Instructions.Add(nop); cilOffset += nop.Size; @@ -176,7 +174,6 @@ private void ReadInstructions(VMMethod vmMethod) new CilInstruction(vmOpCode.CilOpCode.Value, operand); var vmStart = (uint)vmMethod.CurrentVirtualOffset; instruction.Offset = cilOffset; - vmMethod.InstructionVirtualOffsets.Add(vmStart); vmMethod.VmToCilOffsetMap[vmStart] = cilOffset; vmMethod.Instructions.Add(instruction); cilOffset += instruction.Size; @@ -370,8 +367,8 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) { if (!vmMethod.HasHomomorphicEncryption) { - vmMethod.HMEndPositionStack = new Stack(); vmMethod.HasHomomorphicEncryption = true; + vmMethod.HMEndPositionStack = new Stack(); } try @@ -430,7 +427,7 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) var decryptor = new HMDecryptor(pwdEntry.Bytes, salt); var decrypted = decryptor.DecryptInstructionBlock(VMStream); - vmMethod.HMEndPositionStack.Push(vmMethod.CurrentVirtualOffset + decrypted.Length); + vmMethod.HMEndPositionStack.Push((uint)(vmMethod.CurrentVirtualOffset + decrypted.Length)); // Swap reader to decrypted bytes and push current for nested blocks. _readerStack.Push(_currentReader); From 252671d4601d7649ee7efc13be4de466d2cb6f9c Mon Sep 17 00:00:00 2001 From: puff <33184334+puff@users.noreply.github.com> Date: Fri, 19 Sep 2025 06:00:05 -0400 Subject: [PATCH 5/6] Support multiple HM passwords per method, and force their type to be specified. --- README.md | 26 +++++- .../Options/DevirtualizationOptions.cs | 10 ++- .../Options/DevirtualizationOptionsBinder.cs | 84 ++++++++++++++----- .../Pipeline/MethodDevirtualizer.cs | 56 ++++++------- src/EazyDevirt/Program.cs | 2 +- 5 files changed, 125 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 48a0e4b..a1a3e61 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, repeatable. Formats: mdtoken:order:type:value | mdtoken:type:value. Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. Strings use UTF-16. Passwords are consumed per method in specified order. --version Show version information -?, -h, --help Show help and usage information ``` @@ -43,6 +44,29 @@ Options: $ EazyDevirt.exe test.exe -v 3 --preserve-all --save-anyway true ``` +### Homomorphic Encryption passwords (--hm-pass) +- 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 (typed-only): +- `mdtoken:order:type:value` +- `mdtoken:type:value` + +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 uses UTF-16 +EazyDevirt.exe app.exe \ + --hm-pass 06000123:int:1337 \ + --hm-pass 06000456:string:MySecret +``` + +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 +122,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/Devirtualization/Options/DevirtualizationOptions.cs b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs index 46db2d8..b76bf98 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptions.cs @@ -73,11 +73,13 @@ internal record DevirtualizationOptions public bool RequireDepsForGenericMethods { get; init; } /// - /// Dictionary of homomorphic encryption passwords keyed by method metadata token (mdtoken). - /// Provided via CLI as "--hm-pass mdtoken:type:value" (repeatable). + /// 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(); + public Dictionary> HmPasswords { get; init; } = new(); } /// @@ -99,4 +101,4 @@ internal enum NumericKind /// /// Represents a typed HM password value and its precomputed big-endian bytes. /// -internal sealed record HmPasswordEntry(NumericKind Kind, string Value, byte[] Bytes); \ No newline at end of file +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 a1b3774..0e0e906 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs @@ -1,4 +1,3 @@ -using System; using System.CommandLine; using System.CommandLine.Binding; using System.Globalization; @@ -50,18 +49,20 @@ protected override DevirtualizationOptions GetBoundValue(BindingContext bindingC HmPasswords = ParseHmPasswords(bindingContext) }; - private Dictionary ParseHmPasswords(BindingContext bindingContext) + private Dictionary> ParseHmPasswords(BindingContext bindingContext) { - var dict = new Dictionary(); + // Temporary structure to retain insertion order for implicit-order entries. + var tmp = new Dictionary>(); var entries = bindingContext.ParseResult.GetValueForOption(_hmPasswordsOption) ?? Array.Empty(); foreach (var entry in entries) { if (string.IsNullOrWhiteSpace(entry)) continue; - // Accept formats: - // 1) mdtoken:type:value - // 2) mdtoken:value (fallback, auto-width) - var parts = entry.Split(':', 3, StringSplitOptions.TrimEntries); + + // Accept formats + // 1) mdtoken:order:type:value + // 2) mdtoken:type:value + var parts = entry.Split(':', 4, StringSplitOptions.TrimEntries); if (parts.Length < 2) continue; @@ -69,26 +70,71 @@ private Dictionary ParseHmPasswords(BindingContext bindin if (!TryParseMdToken(tokenStr, out var token)) continue; - if (parts.Length == 3) + // 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 typeStr = parts[1]; - var valueStr = parts[2]; - if (!TryMapType(typeStr, out var kind)) + var valueStr = parts[3]; + if (!TryParseTypedNumericToBigEndian(kind1, valueStr, out var bytes1)) continue; - if (!TryParseTypedNumericToBigEndian(kind, valueStr, out var bytes)) - continue; - dict[token] = new HmPasswordEntry(kind, valueStr, bytes); + list.Add((kind1, valueStr, bytes1, ord, seq)); + continue; } - else // parts.Length == 2 -> fallback (auto-width) + + if (parts.Length == 3 && TryMapType(parts[1], out var kind2)) { - var valueStr = parts[1]; - if (!TryParseNumericAutoWidthToBigEndian(valueStr, out var bytes, out var inferredKind)) + var valueStr = parts[2]; + if (!TryParseTypedNumericToBigEndian(kind2, valueStr, out var bytes2)) continue; - dict[token] = new HmPasswordEntry(inferredKind, valueStr, bytes); + list.Add((kind2, valueStr, bytes2, null, seq)); + continue; + } + } + + // 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 dict; + 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) diff --git a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs index 16a72cb..b6e6d48 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -4,13 +4,8 @@ using EazyDevirt.Core.Architecture; using EazyDevirt.Core.Crypto; using EazyDevirt.Core.IO; -using System; -using System.IO; -using System.Linq; using System.Globalization; using System.Text; -using System.Collections.Generic; -using EazyDevirt.PatternMatching.Patterns.OpCodes; using EazyDevirt.Devirtualization.Options; namespace EazyDevirt.Devirtualization.Pipeline; @@ -21,6 +16,7 @@ internal class MethodDevirtualizer : StageBase private VMBinaryReader VMStreamReader { get; set; } private VMBinaryReader _currentReader; private readonly Stack _readerStack = new(); + private readonly Dictionary _hmPasswordIndices = new(); private Resolver Resolver { get; set; } @@ -382,11 +378,11 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) return null; } - // Resolve password from CLI or prompt (typed). + // Resolve password from CLI or prompt. if (!TryGetHomomorphicPassword(vmMethod.Parent.MetadataToken.ToUInt32(), out var pwdEntry)) { - Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Enter homomorphic password (typed):"); - Console.Write("Type (sbyte, byte, short, ushort, int, uint, long, ulong, string) [empty=auto]: "); + Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Enter homomorphic password:"); + 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; @@ -397,29 +393,22 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) return null; } - if (!string.IsNullOrWhiteSpace(typeInput)) + if (string.IsNullOrWhiteSpace(typeInput) || !TryMapType(typeInput, out var kind) || !TryParseTypedNumericToBigEndian(kind, valueInput, out var bytes)) { - if (!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 for string."); - vmMethod.SuccessfullyDevirtualized = false; - return null; - } - pwdEntry = new HmPasswordEntry(kind, valueInput, 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 for string."); + vmMethod.SuccessfullyDevirtualized = false; + return null; } - else + var token = vmMethod.Parent.MetadataToken.ToUInt32(); + if (!Ctx.Options.HmPasswords.TryGetValue(token, out var list)) { - if (!TryParseNumericAutoWidthToBigEndian(valueInput, out var bytes, out var inferredKind)) - { - Ctx.Console.Error($"[{vmMethod.Parent.MetadataToken}] Invalid password value. Provide an integer (decimal or 0xHEX)."); - vmMethod.SuccessfullyDevirtualized = false; - return null; - } - pwdEntry = new HmPasswordEntry(inferredKind, valueInput, bytes); + list = new List(); + Ctx.Options.HmPasswords[token] = list; } - - // Cache for subsequent use within the same run. - Ctx.Options.HmPasswords[vmMethod.Parent.MetadataToken.ToUInt32()] = pwdEntry; + 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); @@ -487,7 +476,18 @@ private bool TryGetHomomorphicPassword(uint mdToken, out HmPasswordEntry pwdEntr if (map is null || map.Count == 0) return false; - return map.TryGetValue(mdToken, out pwdEntry); + 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 TryParseNumericToBigEndian(string s, out byte[] bytes) diff --git a/src/EazyDevirt/Program.cs b/src/EazyDevirt/Program.cs index 9450484..da81ca1 100644 --- a/src/EazyDevirt/Program.cs +++ b/src/EazyDevirt/Program.cs @@ -93,7 +93,7 @@ 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. Format: mdtoken:type:value or mdtoken:value (auto-width, may fail). Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. String uses UTF-16. Repeatable.") + 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 uses UTF-16. Repeatable; passwords are consumed in the specified order per method.") { Arity = ArgumentArity.ZeroOrMore }; From 3c30665750781947f5663487d1385497c419c9df Mon Sep 17 00:00:00 2001 From: puff <33184334+puff@users.noreply.github.com> Date: Fri, 19 Sep 2025 08:59:02 -0400 Subject: [PATCH 6/6] Make string HM passwords wrapped in quotes --- README.md | 27 ++- .../Core/Crypto/HMDecryptionChain.cs | 2 +- src/EazyDevirt/Core/Crypto/HMDecryptor.cs | 3 - .../Options/DevirtualizationOptionsBinder.cs | 123 ++++++++------ .../Pipeline/MethodDevirtualizer.cs | 133 +++------------ src/EazyDevirt/Program.cs | 160 +++++++++++++++++- 6 files changed, 275 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index a1a3e61..7beb809 100644 --- a/README.md +++ b/README.md @@ -34,7 +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, repeatable. Formats: mdtoken:order:type:value | mdtoken:type:value. Types: sbyte, byte, short, ushort, int, uint, long, ulong, string. Strings use UTF-16. Passwords are consumed per method in specified order. + --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 ``` @@ -44,24 +44,41 @@ Options: $ EazyDevirt.exe test.exe -v 3 --preserve-all --save-anyway true ``` -### Homomorphic Encryption passwords (--hm-pass) +### 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 (typed-only): +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 uses UTF-16 +# Multiple methods; string must be quoted and use UTF-16 EazyDevirt.exe app.exe \ --hm-pass 06000123:int:1337 \ - --hm-pass 06000456:string:MySecret + --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[]`. diff --git a/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs b/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs index 3ebd957..bd9ff10 100644 --- a/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs +++ b/src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs @@ -15,7 +15,7 @@ protected HMDecryptionChain(byte[] password, byte[] salt) { var pbkdf = new PBKDF2(password, salt, 1); var array = new SymmetricAlgorithm[5]; - for (int i = 0; i < 5; i++) + for (var i = 0; i < 5; i++) { var chain = new SymmetricAlgorithmChain(new Skip32Cipher()); chain.Key = pbkdf.GetBytes(chain.KeySize / 8); diff --git a/src/EazyDevirt/Core/Crypto/HMDecryptor.cs b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs index a2b1dd5..33c4241 100644 --- a/src/EazyDevirt/Core/Crypto/HMDecryptor.cs +++ b/src/EazyDevirt/Core/Crypto/HMDecryptor.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace EazyDevirt.Core.Crypto; internal sealed class HMDecryptor : HMDecryptionChain diff --git a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs index 0e0e906..4ec9161 100644 --- a/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs +++ b/src/EazyDevirt/Devirtualization/Options/DevirtualizationOptionsBinder.cs @@ -34,6 +34,40 @@ public DevirtualizationOptionsBinder(Argument assemblyArgument, Argume _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) => new() { @@ -54,44 +88,75 @@ private Dictionary> ParseHmPasswords(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 + // 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)) + 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 == 3 && TryMapType(parts[1], out var kind2)) + if (parts.Length == 4 && TryParseOrder(parts[1], out _)) { - var valueStr = parts[2]; + 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). @@ -190,7 +255,9 @@ private static bool TryParseTypedNumericToBigEndian(NumericKind kind, string s, s = s.Trim(); if (kind == NumericKind.String) { - bytes = Encoding.Unicode.GetBytes(s); + if (!TryUnquote(s, out var unquoted)) + return false; + bytes = Encoding.Unicode.GetBytes(unquoted); return true; } @@ -307,50 +374,4 @@ private static bool TryParseTypedNumericToBigEndian(NumericKind kind, string s, } } } - - private static bool TryParseNumericAutoWidthToBigEndian(string s, out byte[] bytes, out NumericKind kind) - { - bytes = Array.Empty(); - kind = default; - if (string.IsNullOrWhiteSpace(s)) return false; - s = s.Trim(); - - if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - { - var hex = s[2..]; - if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) - return false; - if (u <= byte.MaxValue) - { bytes = new[] { (byte)u }; kind = NumericKind.Byte; return true; } - if (u <= ushort.MaxValue) - { bytes = new[] { (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt16; return true; } - if (u <= uint.MaxValue) - { bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt32; return true; } - 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 - }; kind = NumericKind.UInt64; return true; - } - - // Decimal: try signed then unsigned to preserve width semantics - if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) - { bytes = new[] { unchecked((byte)sb) }; kind = NumericKind.SByte; return true; } - if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) - { bytes = new[] { b }; kind = NumericKind.Byte; return true; } - if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh)) - { var u = unchecked((ushort)sh); bytes = new[] { (byte)(u >> 8), (byte)u }; kind = NumericKind.Int16; return true; } - if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush)) - { bytes = new[] { (byte)(ush >> 8), (byte)ush }; kind = NumericKind.UInt16; return true; } - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32)) - { var u = unchecked((uint)i32); bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; kind = NumericKind.Int32; return true; } - if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui32)) - { bytes = new[] { (byte)(ui32 >> 24), (byte)(ui32 >> 16), (byte)(ui32 >> 8), (byte)ui32 }; kind = NumericKind.UInt32; return true; } - if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64)) - { 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 }; kind = NumericKind.Int64; return true; } - if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui64)) - { 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 }; kind = NumericKind.UInt64; return true; } - - 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 b6e6d48..b5e5f70 100644 --- a/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs +++ b/src/EazyDevirt/Devirtualization/Pipeline/MethodDevirtualizer.cs @@ -381,7 +381,10 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) // Resolve password from CLI or prompt. if (!TryGetHomomorphicPassword(vmMethod.Parent.MetadataToken.ToUInt32(), out var pwdEntry)) { - Ctx.Console.Info($"[{vmMethod.Parent.MetadataToken}] Enter homomorphic password:"); + 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): "); @@ -395,11 +398,10 @@ private void ResolveExceptionHandlers(VMMethod vmMethod) 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 for string."); + 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; } - var token = vmMethod.Parent.MetadataToken.ToUInt32(); if (!Ctx.Options.HmPasswords.TryGetValue(token, out var list)) { list = new List(); @@ -490,55 +492,6 @@ private bool TryGetHomomorphicPassword(uint mdToken, out HmPasswordEntry pwdEntr return true; } - private static bool TryParseNumericToBigEndian(string s, out byte[] bytes) - { - bytes = Array.Empty(); - if (string.IsNullOrWhiteSpace(s)) return false; - s = s.Trim(); - - // Hex prefixed value => parse as unsigned and choose minimal width - if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - { - var hex = s[2..]; - if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) - return false; - - if (u <= byte.MaxValue) - bytes = new[] { (byte)u }; - else if (u <= ushort.MaxValue) - bytes = new[] { (byte)(u >> 8), (byte)u }; - else if (u <= uint.MaxValue) - bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; - else - 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; - } - - // Decimal: try signed then unsigned types to preserve width semantics - if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) - { bytes = new[] { unchecked((byte)sb) }; return true; } - if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) - { bytes = new[] { b }; return true; } - if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh)) - { var u = unchecked((ushort)sh); bytes = new[] { (byte)(u >> 8), (byte)u }; return true; } - if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush)) - { bytes = new[] { (byte)(ush >> 8), (byte)ush }; return true; } - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32)) - { var u = unchecked((uint)i32); bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; return true; } - if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui32)) - { bytes = new[] { (byte)(ui32 >> 24), (byte)(ui32 >> 16), (byte)(ui32 >> 8), (byte)ui32 }; return true; } - if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64)) - { 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; } - if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui64)) - { 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; } - - return false; - } - private static bool TryMapType(string s, out NumericKind kind) { kind = default; @@ -611,68 +564,28 @@ private static bool TryParseTypedNumericToBigEndian(NumericKind kind, string s, 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; - } - } - } - private static bool TryParseNumericAutoWidthToBigEndian(string s, out byte[] bytes, out NumericKind kind) - { - bytes = Array.Empty(); - kind = default; - if (string.IsNullOrWhiteSpace(s)) return false; - s = s.Trim(); - if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + switch (kind) { - var hex = s[2..]; - if (!ulong.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var u)) + 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; - if (u <= byte.MaxValue) - { bytes = new[] { (byte)u }; kind = NumericKind.Byte; return true; } - if (u <= ushort.MaxValue) - { bytes = new[] { (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt16; return true; } - if (u <= uint.MaxValue) - { bytes = new[] { (byte)(u >> 24), (byte)(u >> 16), (byte)(u >> 8), (byte)u }; kind = NumericKind.UInt32; return true; } - 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 }; kind = NumericKind.UInt64; return true; } - if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb2)) - { bytes = new[] { unchecked((byte)sb2) }; kind = NumericKind.SByte; return true; } - if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b2)) - { bytes = new[] { b2 }; kind = NumericKind.Byte; return true; } - if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sh2)) - { var u2 = unchecked((ushort)sh2); bytes = new[] { (byte)(u2 >> 8), (byte)u2 }; kind = NumericKind.Int16; return true; } - if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ush2)) - { bytes = new[] { (byte)(ush2 >> 8), (byte)ush2 }; kind = NumericKind.UInt16; return true; } - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i322)) - { var u3 = unchecked((uint)i322); bytes = new[] { (byte)(u3 >> 24), (byte)(u3 >> 16), (byte)(u3 >> 8), (byte)u3 }; kind = NumericKind.Int32; return true; } - if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui322)) - { bytes = new[] { (byte)(ui322 >> 24), (byte)(ui322 >> 16), (byte)(ui322 >> 8), (byte)ui322 }; kind = NumericKind.UInt32; return true; } - if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i642)) - { var u4 = unchecked((ulong)i642); bytes = new[] { (byte)(u4 >> 56), (byte)(u4 >> 48), (byte)(u4 >> 40), (byte)(u4 >> 32), (byte)(u4 >> 24), (byte)(u4 >> 16), (byte)(u4 >> 8), (byte)u4 }; kind = NumericKind.Int64; return true; } - if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui642)) - { bytes = new[] { (byte)(ui642 >> 56), (byte)(ui642 >> 48), (byte)(ui642 >> 40), (byte)(ui642 >> 32), (byte)(ui642 >> 24), (byte)(ui642 >> 16), (byte)(ui642 >> 8), (byte)ui642 }; kind = NumericKind.UInt64; return true; } - return false; } private object? ReadInlineTok(VMOpCode vmOpCode) => diff --git a/src/EazyDevirt/Program.cs b/src/EazyDevirt/Program.cs index da81ca1..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,10 +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 uses UTF-16. Repeatable; passwords are consumed in the specified order per method.") + 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")