Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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
```
Expand All @@ -43,6 +44,46 @@ Options:
$ EazyDevirt.exe test.exe -v 3 --preserve-all --save-anyway true
```

### Homomorphic Encryption passwords
You can either provide the passwords using the CLI or the interactive prompt.
If you're using the interactive prompt, you don't need to quote string values, but the rules below regarding the types and numeric values still apply.

- Provide one or more passwords per method using the metadata token (hex, with or without `0x`).
- Typed-only: you must specify the type. Supported: `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `string`.
- Repeat the option for multiple passwords. Use optional 1-based `order` to control sequence when a method has multiple Homomorphic Encryption blocks.

Formats:
- `mdtoken:order:type:value`
- `mdtoken:type:value`

Validation rules:
- `mdtoken` must be hex (with or without `0x`), e.g. `0x06000123` or `06000123`.
- `order` (if present) must be a positive integer (1-based).
- `type` must be one of the supported types. Aliases are accepted:
- `sbyte`/`i8`, `byte`/`u8`, `short`/`int16`/`i16`, `ushort`/`uint16`/`u16`,
`int`/`int32`/`i32`, `uint`/`uint32`/`u32`, `long`/`int64`/`i64`, `ulong`/`uint64`/`u64`, `string`/`str`.
- `value` must match the specified type:
- Numerics: decimal (e.g. `1337`) or hex with `0x` prefix (e.g. `0xDEADBEEF`).
- String: any text wrapped in double quotes ("..."). Quotes are required; colons are allowed inside. Only double quotes and backslashes must be escaped via backslash (i.e., use `\"` for `"` and `\\` for `\`). Strings are encoded as UTF-16.
- If `order` is omitted, passwords are consumed in the order they appear on the CLI for that `mdtoken`.

Examples:
```console
# Two explicitly ordered numeric passwords for method 0x06000123
EazyDevirt.exe app.exe --hm-pass 0x06000123:1:uint:1234 --hm-pass 0x06000123:2:ulong:0xDEADBEEF

# Multiple methods; string must be quoted and use UTF-16
EazyDevirt.exe app.exe \
--hm-pass 06000123:int:1337 \
--hm-pass 06000456:string:"My Secret Password"

# Strings with quotes inside (escape double quotes):
EazyDevirt.exe app.exe --hm-pass 06000456:string:"Password is: \"Hello\""
```

It should be noted there are two additional password types that are not supported by [EazyDevirt]: `IEnumerable` and `byte[]`.
Feel free to make a PR if you need support for them.

### Notes
Don't rename any members before devirtualization, as [Eazfuscator.NET] resolves members using names rather than tokens.

Expand Down Expand Up @@ -98,4 +139,4 @@ And a thank you, to [all other contributors](https://github.com/puff/EazyDevirt/
[AsmResolver]:https://github.com/Washi1337/AsmResolver
[Echo]:https://github.com/Washi1337/Echo
[Eazfuscator.NET]:https://www.gapotchenko.com/eazfuscator.net
[EazFixer]:https://github.com/holly-hacker/EazFixer
[EazFixer]:https://github.com/holly-hacker/EazFixer
13 changes: 10 additions & 3 deletions src/EazyDevirt/Core/Architecture/VMMethod.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using AsmResolver.DotNet;
using AsmResolver.DotNet;
using AsmResolver.DotNet.Code.Cil;
using AsmResolver.PE.DotNet.Cil;

Expand All @@ -17,12 +17,19 @@ internal record VMMethod(MethodDefinition Parent, string EncodedMethodKey, long
public List<CilLocalVariable> Locals { get; set; }
public List<CilInstruction> Instructions { get; set; }


public bool SuccessfullyDevirtualized { get; set; }
public bool HasHomomorphicEncryption { get; set; }
public int CodeSize { get; set; }
public long CodePosition { get; set; }
public uint CurrentVirtualOffset { get; set; }
/// <summary>
/// Mapping from VM virtual offset to CIL offset, built once during instruction reading.
/// </summary>
public Dictionary<uint, int> VmToCilOffsetMap { get; set; }
public long InitialCodeStreamPosition { get; set; }
/// <summary>
/// Stack holding Homomorphic Encryption ending positions. Used to calculate virtual <-> CIL offsets.
/// </summary>
public Stack<uint> HMEndPositionStack { get; set; }

public override string ToString() =>
$"Parent: {Parent.MetadataToken} | EncodedMethodKey: {EncodedMethodKey} | MethodKey: 0x{MethodKey:X} | " +
Expand Down
108 changes: 108 additions & 0 deletions src/EazyDevirt/Core/Crypto/HMDecryptionChain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Security.Cryptography;

namespace EazyDevirt.Core.Crypto;

internal abstract class HMDecryptionChain
{
private readonly SymmetricAlgorithm[] _algorithmChains;

protected HMDecryptionChain(byte[] password, long salt)
: this(password, ConvertLongToLittleEndian(salt))
{
}

protected HMDecryptionChain(byte[] password, byte[] salt)
{
var pbkdf = new PBKDF2(password, salt, 1);
var array = new SymmetricAlgorithm[5];
for (var i = 0; i < 5; i++)
{
var chain = new SymmetricAlgorithmChain(new Skip32Cipher());
chain.Key = pbkdf.GetBytes(chain.KeySize / 8);
chain.IV = pbkdf.GetBytes(chain.GetIVSize() / 8);
array[i] = chain;
}

_algorithmChains = array;
}

protected static int AlignToMultipleOf4(int value) => (value + 3) / 4 * 4;

public static int MinAlignToMultipleOf4(int value) => AlignToMultipleOf4(value + 4);

protected static byte[] ConvertLongToLittleEndian(long value)
{
var output = new byte[8];
ConvertLongToLittleEndian(value, output, 0);
return output;
}

protected static void ConvertLongToLittleEndian(long value, byte[] output, int startIndex)
{
output[startIndex] = (byte)value;
output[startIndex + 1] = (byte)(value >> 8);
output[startIndex + 2] = (byte)(value >> 16);
output[startIndex + 3] = (byte)(value >> 24);
output[startIndex + 4] = (byte)(value >> 32);
output[startIndex + 5] = (byte)(value >> 40);
output[startIndex + 6] = (byte)(value >> 48);
output[startIndex + 7] = (byte)(value >> 56);
}

protected static int ConvertInt32BytesToLittleEndian(byte[] bytes, int startIndex)
{
return bytes[startIndex]
| (bytes[startIndex + 1] << 8)
| (bytes[startIndex + 2] << 16)
| (bytes[startIndex + 3] << 24);
}

protected static void ConvertInt32ToLittleEndian(int value, byte[] output, int startIndex)
{
output[startIndex] = (byte)value;
output[startIndex + 1] = (byte)(value >> 8);
output[startIndex + 2] = (byte)(value >> 16);
output[startIndex + 3] = (byte)(value >> 24);
}

protected byte[] DecryptBytes(byte[] input, bool startWithEncrypt)
{
if (startWithEncrypt)
{
foreach (var alg in _algorithmChains)
{
if (startWithEncrypt)
{
using var enc = alg.CreateEncryptor();
input = enc.TransformFinalBlock(input, 0, input.Length);
}
else
{
using var dec = alg.CreateDecryptor();
input = dec.TransformFinalBlock(input, 0, input.Length);
}
startWithEncrypt = !startWithEncrypt;
}
}
else
{
for (int i = _algorithmChains.Length - 1; i >= 0; i--)
{
var alg = _algorithmChains[i];
if (startWithEncrypt)
{
using var enc = alg.CreateEncryptor();
input = enc.TransformFinalBlock(input, 0, input.Length);
}
else
{
using var dec = alg.CreateDecryptor();
input = dec.TransformFinalBlock(input, 0, input.Length);
}
startWithEncrypt = !startWithEncrypt;
}
}

return input;
}
}
48 changes: 48 additions & 0 deletions src/EazyDevirt/Core/Crypto/HMDecryptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace EazyDevirt.Core.Crypto;

internal sealed class HMDecryptor : HMDecryptionChain
{
public HMDecryptor(byte[] password, long salt) : base(password, salt)
{
}

public byte[] DecryptInstructionBlock(Stream instructionsStream)
{
// Read first 4 bytes (encrypted header containing original length)
var header = new byte[4];
ReadBytes(instructionsStream, header, 0, 4);

// Decrypt header to obtain original size
var decryptedHeader = DecryptBytes(header, startWithEncrypt: false);
var originalSize = ConvertInt32BytesToLittleEndian(decryptedHeader, 0);

// Total encrypted size is aligned to 4 and includes 4-byte header
var alignedTotal = MinAlignToMultipleOf4(originalSize);
var remaining = alignedTotal - 4;

var fullBlock = new byte[alignedTotal];
Buffer.BlockCopy(header, 0, fullBlock, 0, 4);

// Read remaining encrypted bytes
ReadBytes(instructionsStream, fullBlock, 4, remaining);

// Decrypt full block then strip 4-byte header
var decrypted = DecryptBytes(fullBlock, startWithEncrypt: false);
var result = new byte[originalSize];
Buffer.BlockCopy(decrypted, 4, result, 0, originalSize);
return result;
}

private static void ReadBytes(Stream stream, byte[] buffer, int offset, int count)
{
var remaining = count;
while (remaining > 0)
{
var read = stream.Read(buffer, offset, remaining);
if (read <= 0)
throw new EndOfStreamException("Unexpected end of stream while reading encrypted homomorphic block.");
offset += read;
remaining -= read;
}
}
}
131 changes: 131 additions & 0 deletions src/EazyDevirt/Core/Crypto/PBKDF2.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading