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")