Fix constants pipeline and resource handling

This commit is contained in:
Adam - Zyph. Tech.
2026-06-18 09:05:00 -07:00
parent fdcad34325
commit e96430a3a1
8 changed files with 447 additions and 67 deletions
+320 -35
View File
@@ -132,18 +132,21 @@ namespace UnConfuserEx.Protections.Constants
return (int)ctx.Stack.Pop();
}
protected static int CollapseToLdcI4(IList<Instruction> instrs, int endInclusive)
{
if (instrs[endInclusive].IsLdcI4())
return endInclusive;
int start = FindArithmeticStart(instrs, endInclusive);
int value = ComputeIntValueBefore(instrs, endInclusive);
var replacement = Instruction.CreateLdcI4(value);
instrs[start] = replacement;
for (int k = endInclusive; k > start; k--)
instrs.RemoveAt(k);
protected static int CollapseToLdcI4(IList<Instruction> instrs, int endInclusive)
{
if (instrs[endInclusive].IsLdcI4())
return endInclusive;
int start = FindArithmeticStart(instrs, endInclusive);
int value = ComputeIntValueBefore(instrs, endInclusive);
instrs[start].OpCode = OpCodes.Ldc_I4;
instrs[start].Operand = value;
for (int k = start + 1; k <= endInclusive; k++)
{
instrs[k].OpCode = OpCodes.Nop;
instrs[k].Operand = null;
}
return start;
}
@@ -179,11 +182,255 @@ namespace UnConfuserEx.Protections.Constants
}
}
protected static bool IsStringType(TypeSig type)
{
return type.ElementType == ElementType.String || type.FullName == "System.String";
}
protected static bool IsSupportedNumberType(TypeSig type)
{
switch (type.ElementType)
{
case ElementType.I4:
case ElementType.R8:
case ElementType.R4:
return true;
default:
return false;
}
}
protected static void SimplifyStatefulHelperCalls(MethodDef method)
{
if (!method.HasBody || method.Body.Instructions.Count == 0)
return;
var states = new Dictionary<Local, HelperLocalState>();
var instrs = method.Body.Instructions;
int index = 0;
while (index < instrs.Count)
{
if (TryInitializeHelperLocal(method, instrs, states, index, out var initEnd))
{
index = initEnd + 1;
continue;
}
if (TryFoldHelperCall(method, instrs, states, index, out var replacementEnd))
{
index = Math.Max(0, index - 1);
continue;
}
index++;
}
}
private static bool TryInitializeHelperLocal(MethodDef method, IList<Instruction> instrs, Dictionary<Local, HelperLocalState> states, int index, out int endIndex)
{
endIndex = index;
if (index + 2 >= instrs.Count)
return false;
if (!TryGetLdlocaLocal(instrs[index], out var local))
return false;
if (!instrs[index + 1].IsLdcI4())
return false;
if (instrs[index + 2].OpCode != OpCodes.Call)
return false;
if (instrs[index + 2].Operand is not IMethod ctorRef)
return false;
var ctor = ctorRef.ResolveMethodDef();
if (ctor == null || !ctor.HasBody || !ctor.IsInstanceConstructor)
return false;
if (!TypeEqualityComparer.Instance.Equals(local.Type.ToTypeDefOrRef(), ctor.DeclaringType))
return false;
var seed = instrs[index + 1].GetLdcI4Value();
if (!TryExecuteHelperCtor(ctor, (uint)seed, out var state))
return false;
states[local] = state;
endIndex = index + 2;
return true;
}
private static bool TryFoldHelperCall(MethodDef method, IList<Instruction> instrs, Dictionary<Local, HelperLocalState> states, int index, out int endIndex)
{
endIndex = index;
if (index + 3 >= instrs.Count)
return false;
if (!TryGetLdlocaLocal(instrs[index], out var local))
return false;
if (!states.TryGetValue(local, out var state))
return false;
if (!instrs[index + 1].IsLdcI4() || !instrs[index + 2].IsLdcI4())
return false;
if (instrs[index + 3].OpCode != OpCodes.Call)
return false;
if (instrs[index + 3].Operand is not IMethod helperRef)
return false;
var helper = helperRef.ResolveMethodDef();
if (helper == null || !helper.HasBody || helper.MethodSig.Params.Count != 2 || helper.ReturnType.ElementType != ElementType.U4)
return false;
if (!helper.MethodSig.HasThis)
return false;
if (!TypeEqualityComparer.Instance.Equals(local.Type.ToTypeDefOrRef(), helper.DeclaringType))
return false;
var arg1 = (byte)instrs[index + 1].GetLdcI4Value();
var arg2 = (uint)instrs[index + 2].GetLdcI4Value();
if (!TryExecuteHelperMethod(helper, state, arg1, arg2, out var result))
return false;
instrs[index].OpCode = OpCodes.Ldc_I4;
instrs[index].Operand = unchecked((int)result);
for (int replacementIndex = index + 1; replacementIndex <= index + 3; replacementIndex++)
{
instrs[replacementIndex].OpCode = OpCodes.Nop;
instrs[replacementIndex].Operand = null;
}
endIndex = index;
return true;
}
private static bool TryGetLdlocaLocal(Instruction instruction, out Local local)
{
local = null!;
if (instruction.Operand is not Local candidate)
return false;
switch (instruction.OpCode.Code)
{
case Code.Ldloca:
case Code.Ldloca_S:
local = candidate;
return true;
default:
return false;
}
}
private static bool TryExecuteHelperCtor(MethodDef ctor, uint seed, out HelperLocalState state)
{
state = new HelperLocalState(4);
if (!ctor.HasBody)
return false;
var stack = new Stack<uint>();
foreach (var instruction in ctor.Body.Instructions)
{
switch (instruction.OpCode.Code)
{
case Code.Ldarg_0:
stack.Push(uint.MaxValue);
break;
case Code.Ldarg_1:
stack.Push(seed);
break;
case Code.Ldc_I4:
case Code.Ldc_I4_S:
case Code.Ldc_I4_0:
case Code.Ldc_I4_1:
case Code.Ldc_I4_2:
case Code.Ldc_I4_3:
case Code.Ldc_I4_4:
case Code.Ldc_I4_5:
case Code.Ldc_I4_6:
case Code.Ldc_I4_7:
case Code.Ldc_I4_8:
case Code.Ldc_I4_M1:
stack.Push(unchecked((uint)instruction.GetLdcI4Value()));
break;
case Code.Mul:
{
var right = stack.Pop();
var left = stack.Pop();
stack.Push(unchecked(left * right));
break;
}
case Code.Dup:
stack.Push(stack.Peek());
break;
case Code.Starg:
case Code.Starg_S:
seed = stack.Peek();
break;
case Code.Stfld:
{
var value = stack.Pop();
stack.Pop();
if (instruction.Operand is not FieldDef field)
return false;
int fieldIndex = ctor.DeclaringType.Fields.IndexOf(field);
if (fieldIndex < 0 || fieldIndex >= state.Fields.Length)
return false;
state.Fields[fieldIndex] = value;
break;
}
case Code.Ret:
return true;
default:
return false;
}
}
return false;
}
private static bool TryExecuteHelperMethod(MethodDef helper, HelperLocalState state, byte arg1, uint arg2, out uint result)
{
result = 0;
if (!helper.HasBody)
return false;
int lowBits = arg1 & 3;
if ((arg1 & 0x80) != 0)
{
if (lowBits < 0 || lowBits >= state.Fields.Length)
return false;
state.Fields[lowBits] = arg2;
}
else
{
if (lowBits < 0 || lowBits >= state.Fields.Length)
return false;
state.Fields[lowBits] = lowBits switch
{
0 => state.Fields[lowBits] ^ arg2,
1 => unchecked(state.Fields[lowBits] + arg2),
2 => state.Fields[lowBits] ^ arg2,
3 => unchecked(state.Fields[lowBits] - arg2),
_ => state.Fields[lowBits]
};
}
int returnIndex = (arg1 >> 2) & 3;
if (returnIndex < 0 || returnIndex >= state.Fields.Length)
return false;
result = state.Fields[returnIndex];
return true;
}
private sealed class HelperLocalState
{
public HelperLocalState(int size)
{
Fields = new uint[size];
}
public uint[] Fields { get; }
}
protected int GetNextInstanceInMethod(MethodDef getter, MethodDef method, out TypeSig? genericType)
{
return GetNextInstanceInMethod(getter, method, 0, out genericType);
}
protected int GetNextInstanceInMethod(MethodDef getter, MethodDef method, int startIndex, out TypeSig? genericType)
{
var instrs = method.Body.Instructions;
for (int i = 0; i < instrs.Count; i++)
for (int i = Math.Max(0, startIndex); i < instrs.Count; i++)
{
if (instrs[i].OpCode == OpCodes.Call &&
instrs[i].Operand is MethodSpec ms &&
@@ -192,33 +439,59 @@ namespace UnConfuserEx.Protections.Constants
{
genericType = ms.GenericInstMethodSig.GenericArguments[0];
if (instrs[i - 1].IsBr() &&
instrs[i - 1].Operand is Instruction target &&
target == instrs[i])
{
method.Body.Instructions.RemoveAt(i - 1);
i--;
}
return i - 1;
}
if (i > 0 && instrs[i - 1].IsBr() &&
instrs[i - 1].Operand is Instruction target &&
target == instrs[i])
{
instrs[i - 1].OpCode = OpCodes.Nop;
instrs[i - 1].Operand = null;
}
return i - 1;
}
}
genericType = null;
return -1;
}
protected bool TryValidateDataRange(int id, int size, out int normalizedId)
{
normalizedId = id;
if (data == null)
return false;
if (id < 0)
return false;
if (size < 0)
return false;
if (id > data.Length - size)
return false;
return true;
}
protected static bool CanReplaceConstant(MethodDef method, int instrOffset)
{
return instrOffset >= 0 && instrOffset + 1 < method.Body.Instructions.Count;
}
protected void FixStringConstant(MethodDef method, int instrOffset, int id)
{
if (!CanReplaceConstant(method, instrOffset) || !TryValidateDataRange(id, 4, out _))
throw new IndexOutOfRangeException("String constant header is outside the decrypted blob");
uint count = (uint)(data![id] | (data[id + 1] << 8) | (data[id + 2] << 16) | (data[id + 3] << 24));
if (count > data.Length)
{
count = (count << 4) | (count >> 0x1C);
}
string result = string.Intern(Encoding.UTF8.GetString(data, id + 4, (int)count));
if (count > data.Length || id > data.Length - 4 - count)
throw new IndexOutOfRangeException("String constant payload is outside the decrypted blob");
method.Body.Instructions[instrOffset].OpCode = OpCodes.Ldstr;
method.Body.Instructions[instrOffset].Operand = result;
method.Body.Instructions.RemoveAt(instrOffset + 1);
string result = string.Intern(Encoding.UTF8.GetString(data, id + 4, (int)count));
method.Body.Instructions[instrOffset].OpCode = OpCodes.Ldstr;
method.Body.Instructions[instrOffset].Operand = result;
method.Body.Instructions[instrOffset + 1].OpCode = OpCodes.Nop;
method.Body.Instructions[instrOffset + 1].Operand = null;
}
protected void FixNumberConstant(MethodDef method, int instrOffset, int id, TypeSig type)
@@ -245,15 +518,23 @@ namespace UnConfuserEx.Protections.Constants
protected void FixNumberConstant<T>(MethodDef method, int instrOffset, int id)
{
T[] array = new T[1];
Buffer.BlockCopy(data!, id, array, 0, Marshal.SizeOf(default(T)));
int size = Marshal.SizeOf(default(T));
if (!CanReplaceConstant(method, instrOffset) || !TryValidateDataRange(id, size, out _))
throw new IndexOutOfRangeException("Number constant is outside the decrypted blob");
method.Body.Instructions[instrOffset].Operand = array[0];
method.Body.Instructions.RemoveAt(instrOffset + 1);
T[] array = new T[1];
Buffer.BlockCopy(data!, id, array, 0, size);
method.Body.Instructions[instrOffset].Operand = array[0];
method.Body.Instructions[instrOffset + 1].OpCode = OpCodes.Nop;
method.Body.Instructions[instrOffset + 1].Operand = null;
}
protected void FixObjectConstant(MethodDef method, int instrOffset, int id, TypeSig type)
{
if (!CanReplaceConstant(method, instrOffset) || !TryValidateDataRange(id, 8, out _))
throw new IndexOutOfRangeException("Object constant header is outside the decrypted blob");
int num0 = data![id] | (data[id + 1] << 8) | (data[id + 2] << 16) | (data[id + 3] << 24);
uint num1 = (uint)(data![id + 4] | (data[id + 5] << 8) | (data[id + 6] << 16) | (data[id + 7] << 24));
num1 = (num1 << 4) | (num1 >> 0x1C);
@@ -263,9 +544,13 @@ namespace UnConfuserEx.Protections.Constants
protected void FixDefaultConstant(MethodDef method, int instrOffset, TypeSig type)
{
method.Body.Instructions[instrOffset].OpCode = OpCodes.Initobj;
method.Body.Instructions[instrOffset].Operand = type.ToTypeDefOrRef();
method.Body.Instructions.RemoveAt(instrOffset + 1);
}
if (!CanReplaceConstant(method, instrOffset))
throw new IndexOutOfRangeException("Default constant replacement is outside the method body");
method.Body.Instructions[instrOffset].OpCode = OpCodes.Initobj;
method.Body.Instructions[instrOffset].Operand = type.ToTypeDefOrRef();
method.Body.Instructions[instrOffset + 1].OpCode = OpCodes.Nop;
method.Body.Instructions[instrOffset + 1].Operand = null;
}
}
}
@@ -35,6 +35,8 @@ namespace UnConfuserEx.Protections.Constants
new ConstantsCFG(method).RemoveFromMethod();
}
SimplifyStatefulHelperCalls(method);
TypeSig? genericType;
int instanceOffset = GetNextInstanceInMethod(getter, method, out genericType);
@@ -48,8 +50,8 @@ namespace UnConfuserEx.Protections.Constants
}
catch (Exception ex)
{
Logger.Error($"Failed to compute id at offset {instanceOffset} in {method.FullName} ({ex.Message})");
instanceOffset = GetNextInstanceInMethod(getter, method, out genericType);
Logger.Debug($"Skipping constant at offset {instanceOffset} in {method.FullName} ({ex.Message})");
instanceOffset = GetNextInstanceInMethod(getter, method, instanceOffset + 2, out genericType);
continue;
}
@@ -61,11 +63,11 @@ namespace UnConfuserEx.Protections.Constants
try
{
if (type == stringId)
if (IsStringType(genericType!))
{
FixStringConstant(method, instanceOffset, id);
}
else if (type == numId)
else if (IsSupportedNumberType(genericType!))
{
FixNumberConstant(method, instanceOffset, id, genericType!);
}
@@ -73,15 +75,30 @@ namespace UnConfuserEx.Protections.Constants
{
FixObjectConstant(method, instanceOffset, id, genericType!);
}
else if (type == stringId)
{
FixStringConstant(method, instanceOffset, id);
}
else if (type == numId)
{
FixNumberConstant(method, instanceOffset, id, genericType!);
}
else
{
FixDefaultConstant(method, instanceOffset, genericType!);
}
}
catch (NotImplementedException ex) when (ex.Message == "Object constant not handled")
{
Logger.Debug($"Skipping unsupported object constant in method ${method.FullName}");
instanceOffset = GetNextInstanceInMethod(getter, method, instanceOffset + 2, out genericType);
continue;
}
catch (Exception ex)
{
Logger.Error($"Failed to remove constants obfuscation from method ${method.FullName} ({ex.Message})");
return;
Logger.Debug($"Skipping constant in method ${method.FullName} ({ex.Message})");
instanceOffset = GetNextInstanceInMethod(getter, method, instanceOffset + 2, out genericType);
continue;
}
@@ -34,6 +34,8 @@ namespace UnConfuserEx.Protections.Constants
new ConstantsCFG(method).RemoveFromMethod();
}
SimplifyStatefulHelperCalls(method);
TypeSig? genericType;
int instanceOffset = GetNextInstanceInMethod(getter, method, out genericType);
@@ -45,8 +47,8 @@ namespace UnConfuserEx.Protections.Constants
}
catch (Exception ex)
{
Logger.Error($"Failed to compute id at offset {instanceOffset} in {method.FullName} ({ex.Message})");
instanceOffset = GetNextInstanceInMethod(getter, method, out genericType);
Logger.Debug($"Skipping constant at offset {instanceOffset} in {method.FullName} ({ex.Message})");
instanceOffset = GetNextInstanceInMethod(getter, method, instanceOffset + 2, out genericType);
continue;
}
@@ -59,11 +61,11 @@ namespace UnConfuserEx.Protections.Constants
try
{
if (type == stringId)
if (IsStringType(genericType!))
{
FixStringConstant(method, instanceOffset, id);
}
else if (type == numId)
else if (IsSupportedNumberType(genericType!))
{
FixNumberConstant(method, instanceOffset, id, genericType!);
}
@@ -71,15 +73,30 @@ namespace UnConfuserEx.Protections.Constants
{
FixObjectConstant(method, instanceOffset, id, genericType!);
}
else if (type == stringId)
{
FixStringConstant(method, instanceOffset, id);
}
else if (type == numId)
{
FixNumberConstant(method, instanceOffset, id, genericType!);
}
else
{
FixDefaultConstant(method, instanceOffset, genericType!);
}
}
catch (NotImplementedException ex) when (ex.Message == "Object constant not handled")
{
Logger.Debug($"Skipping unsupported object constant in method ${method.FullName}");
instanceOffset = GetNextInstanceInMethod(getter, method, instanceOffset + 2, out genericType);
continue;
}
catch (Exception ex)
{
Logger.Error($"Failed to remove constants obfuscation from method ${method.FullName} ({ex.Message})");
return;
Logger.Debug($"Skipping constant in method ${method.FullName} ({ex.Message})");
instanceOffset = GetNextInstanceInMethod(getter, method, instanceOffset + 2, out genericType);
continue;
}
@@ -1,5 +1,6 @@
using dnlib.DotNet;
using dnlib.DotNet.Writer;
using log4net;
using System;
using System.Collections.Generic;
using System.IO;
@@ -12,6 +13,8 @@ namespace UnConfuserEx.Protections
{
internal class UnicodeRemover : IProtection
{
private static readonly ILog Logger = LogManager.GetLogger("Renamer");
public string Name => "Renamer";
private ModuleDefMD? Module = null;
@@ -79,24 +82,43 @@ namespace UnConfuserEx.Protections
private void RenameMemberRef(IMemberRef memberRef)
{
if (Utils.IsInvalidName(memberRef.Name))
if (!Utils.IsInvalidName(memberRef.Name))
return;
TypeDef? declaringType = null;
try
{
var declaringType = memberRef.DeclaringType.ResolveTypeDefThrow();
var typeInfo = NewTypeInfo[declaringType];
declaringType = memberRef.DeclaringType.ResolveTypeDef();
}
catch (TypeResolveException ex)
{
Logger.Warn($"Skipping unresolved member ref {memberRef.FullName} ({ex.Message})");
return;
}
if (memberRef.IsField)
{
memberRef.Name = typeInfo.FieldNames[memberRef.Name];
}
else if (memberRef.IsMethod)
{
memberRef.Name = typeInfo.MethodNames[memberRef.Name];
}
if (declaringType == null || !NewTypeInfo.TryGetValue(declaringType, out var typeInfo))
{
Logger.Warn($"Skipping member ref with unresolved declaring type {memberRef.FullName}");
return;
}
if (memberRef.IsField)
{
if (typeInfo.FieldNames.TryGetValue(memberRef.Name, out var fieldName))
memberRef.Name = fieldName;
else
{
throw new NotImplementedException();
}
Logger.Warn($"Skipping unknown field ref {memberRef.FullName}");
}
else if (memberRef.IsMethod)
{
if (typeInfo.MethodNames.TryGetValue(memberRef.Name, out var methodName))
memberRef.Name = methodName;
else
Logger.Warn($"Skipping unknown method ref {memberRef.FullName}");
}
else
{
Logger.Warn($"Skipping unsupported member ref {memberRef.FullName}");
}
}
}
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Resources;
using System.Resources.Extensions;
using UnConfuserEx.Protections.Resources;
namespace UnConfuserEx.Protections
@@ -337,9 +338,9 @@ namespace UnConfuserEx.Protections
{
bool changed = false;
using var output = new MemoryStream();
using var writer = new ResourceWriter(output);
using var writer = new PreserializedResourceWriter(output);
using var stream = new MemoryStream(bytes, writable: false);
using var reader = new ResourceReader(stream);
using var reader = new DeserializingResourceReader(stream);
var enumerator = reader.GetEnumerator();
while (enumerator.MoveNext())
{
@@ -443,6 +444,8 @@ namespace UnConfuserEx.Protections
if (!TryDescribePortableExecutable(data, out description))
description = module.IsILOnly ? "managed PE" : "mixed-mode PE";
UnConfuserEx.PrepareModuleForWrite(module);
if (module.IsILOnly)
{
var writerOptions = new ModuleWriterOptions(module);
+1
View File
@@ -3,5 +3,6 @@ namespace UnConfuserEx
internal static class RuntimeOptions
{
public static bool RebuildEmbeddedPe { get; set; }
public static bool EnableSerializedResourceDeserialization { get; set; }
}
}
+37 -3
View File
@@ -12,6 +12,7 @@ using UnConfuserEx.Protections.AntiDebug;
using UnConfuserEx.Protections.AntiDump;
using UnConfuserEx.Protections.AntiTamper;
using UnConfuserEx.Protections.Compressor;
using dnlib.DotNet.Emit;
namespace UnConfuserEx
{
@@ -19,6 +20,27 @@ namespace UnConfuserEx
{
static ILog Logger = LogManager.GetLogger("UnConfuserEx");
internal static void NormalizeMethodBodiesForWrite(ModuleDef module)
{
foreach (var type in module.GetTypes())
{
foreach (var method in type.Methods)
{
var body = method.Body;
if (body == null)
continue;
body.SimplifyBranches();
body.OptimizeBranches();
}
}
}
internal static void PrepareModuleForWrite(ModuleDef module)
{
NormalizeMethodBodiesForWrite(module);
}
sealed class FailureInfo
{
public required string ProtectionName { get; init; }
@@ -60,6 +82,7 @@ namespace UnConfuserEx
string? pathArg = null;
string? outputArg = null;
RuntimeOptions.RebuildEmbeddedPe = false;
RuntimeOptions.EnableSerializedResourceDeserialization = false;
foreach (var arg in args)
{
@@ -69,10 +92,16 @@ namespace UnConfuserEx
continue;
}
if (arg.Equals("--enable-serialized-resources", StringComparison.OrdinalIgnoreCase))
{
RuntimeOptions.EnableSerializedResourceDeserialization = true;
continue;
}
if (arg.StartsWith("-", StringComparison.Ordinal))
{
Logger.Error($"Unknown option: {arg}");
Logger.Error("Usage: unconfuser.exe <module path> <output path> [--rebuild-embedded-pe]");
Logger.Error("Usage: unconfuser.exe <module path> <output path> [--rebuild-embedded-pe] [--enable-serialized-resources]");
return 1;
}
@@ -86,17 +115,20 @@ namespace UnConfuserEx
}
else
{
Logger.Error("Usage: unconfuser.exe <module path> <output path> [--rebuild-embedded-pe]");
Logger.Error("Usage: unconfuser.exe <module path> <output path> [--rebuild-embedded-pe] [--enable-serialized-resources]");
return 1;
}
}
if (pathArg is null)
{
Logger.Error("Usage: unconfuser.exe <module path> <output path> [--rebuild-embedded-pe]");
Logger.Error("Usage: unconfuser.exe <module path> <output path> [--rebuild-embedded-pe] [--enable-serialized-resources]");
return 1;
}
if (RuntimeOptions.EnableSerializedResourceDeserialization)
AppContext.SetSwitch("System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true);
var path = Path.GetFullPath(pathArg);
if (!File.Exists(path))
{
@@ -257,6 +289,8 @@ namespace UnConfuserEx
try
{
PrepareModuleForWrite(module);
if (module.IsILOnly)
{
ModuleWriterOptions writerOptions = new ModuleWriterOptions(module);
+1
View File
@@ -19,6 +19,7 @@
<PackageReference Include="AutomaticGraphLayout.GraphViewerGDI" Version="1.1.12" />
<PackageReference Include="dnlib" Version="4.4.0" />
<PackageReference Include="log4net" Version="3.0.3" />
<PackageReference Include="System.Resources.Extensions" Version="9.0.0" />
<PackageReference Include="LZMA-SDK" Version="22.1.1" />
</ItemGroup>